Compare commits

..

No commits in common. "master" and "v1.0.3" have entirely different histories.

137 changed files with 915 additions and 10919 deletions

10
.gitignore vendored
View File

@ -20,13 +20,3 @@ Desktop.ini
efClient.db efClient.db
*.db-journal *.db-journal
**/user.config **/user.config
packages/
installer/output/
dist/
# Кросс-платформенные публикации и артефакты сборки
publish/
build/
installer/output/

View File

@ -1,6 +0,0 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Elfisa.Avalonia/Elfisa.Avalonia.csproj" />
<Project Path="src/Elfisa.Core/Elfisa.Core.csproj" />
</Folder>
</Solution>

View File

@ -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>

View File

@ -1,24 +0,0 @@
# Gitea + принудительные обновления десктопа
Полная инструкция по установке Gitea: в репозитории `source``docs/gitea/`.
Клиент при старте (`ElectroPharmacy_Load`) вызывает Gitea API:
`GET {UpdateCheckBaseUrl}/api/v1/repos/{UpdateRepo}/releases/latest`
Настройки (можно сменить без пересборки через user settings):
| Ключ | По умолчанию |
|------|----------------|
| `UpdateCheckBaseUrl` | `https://git.24pharmdata.ru` |
| `UpdateRepo` | `pharmdata/elfisa-pharmacy` |
| `UpdateApiToken` | пусто (нужен для private releases) |
## Регламент релиза
1. Поднять `AssemblyVersion` / `AssemblyFileVersion` в `Properties/AssemblyInfo.cs`.
2. `git tag vX.Y.Z && git push gitea master --tags`
3. `powershell -File dist\build-release.ps1`
4. Gitea → Releases → New Release → прикрепить ZIP.
Старый клиент с меньшей версией увидит модалку «Доступно новое обновление» и не пустит в работу.

View File

@ -1,108 +0,0 @@
; ЭльФиСА — установщик «Электронная Фармация»
; Кодировка: UTF-8 with BOM
#define MyAppName "Электронная Фармация"
#define MyAppBrand "ЭльФиСА"
#define MyAppVersion "2.0.1"
#define MyAppPublisher "PharmData"
#define MyAppURL "https://cdn.24pharmdata.ru"
#define MyAppSupportURL "https://24pharmdata.ru"
#define MyAppExeName "Электронная Фармация.exe"
#define MyAppId "{{A7C4E2B1-9F3D-4E6A-8B2C-1D5F7A9E0C42}"
[Setup]
AppId={#MyAppId}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppVerName={#MyAppBrand} — {#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppSupportURL}
AppSupportURL={#MyAppSupportURL}
AppUpdatesURL={#MyAppURL}
; Программа — в LocalAppData (без прав админа). База/логи — тоже в профиле пользователя.
DefaultDirName={localappdata}\{#MyAppBrand}\{#MyAppName}
DefaultGroupName={#MyAppBrand}
DisableProgramGroupPage=no
LicenseFile=license.txt
InfoBeforeFile=welcome.txt
OutputDir=output
OutputBaseFilename=ElfisaPharmacy-Setup-{#MyAppVersion}
SetupIconFile=assets\elfisa.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
UninstallDisplayName={#MyAppBrand} — {#MyAppName}
Compression=lzma2/ultra64
SolidCompression=yes
WizardStyle=modern
WizardSizePercent=120
WizardImageFile=assets\wizard-side.bmp
WizardSmallImageFile=assets\wizard-small.bmp
WizardImageStretch=yes
AllowNoIcons=yes
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
ArchitecturesInstallIn64BitMode=x64compatible
VersionInfoVersion={#MyAppVersion}.0
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription=Установщик {#MyAppBrand} — {#MyAppName}
VersionInfoProductName={#MyAppName}
VersionInfoProductVersion={#MyAppVersion}
VersionInfoCopyright=Copyright (C) {#MyAppPublisher}
ShowLanguageDialog=no
DisableWelcomePage=no
DisableDirPage=no
DisableFinishedPage=no
CloseApplications=yes
RestartApplications=no
SetupLogging=yes
[Languages]
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
[Messages]
russian.BeveledLabel={#MyAppBrand} · PharmData
russian.WelcomeLabel1=Добро пожаловать в установку [name]
russian.WelcomeLabel2=Мастер установит [name/ver] на ваш компьютер.%n%nРекомендуется закрыть все другие приложения перед продолжением.%n%nНажмите «Далее», чтобы продолжить.
russian.FinishedLabel=Установка [name] успешно завершена.%n%nПриложение готово к работе. При первом запуске выполните вход в систему.
russian.ClickFinish=Нажмите «Готово», чтобы выйти из мастера установки.
russian.SelectTasksDesc=Выберите дополнительные задачи, затем нажмите «Далее».
russian.SelectTasksLabel2=Какие дополнительные задачи нужно выполнить?
russian.WizardSelectTasks=Дополнительные задачи
russian.WizardReady=Всё готово к установке
russian.ReadyLabel1=Мастер готов установить [name] на ваш компьютер.
russian.ReadyLabel2a=Нажмите «Установить», чтобы продолжить, или «Назад», если хотите просмотреть либо изменить параметры.
russian.InstallingLabel=Подождите, пока [name] устанавливается на ваш компьютер…
russian.StatusExtractFiles=Распаковка файлов…
russian.StatusCreateDirs=Создание папок…
russian.StatusRegisterFiles=Регистрация компонентов…
russian.ConfirmUninstall=Вы действительно хотите удалить %1 и все его компоненты?
russian.UninstalledAll=%1 успешно удалена.
russian.ExitSetupMessage=Установка не завершена. Если вы выйдете сейчас, программа не будет установлена.%n%nВы можете запустить установщик позже.%n%nВыйти из установки?
[CustomMessages]
russian.CreateDesktopIcon=Создать ярлык на рабочем столе
russian.LaunchAfterInstall=Запустить Электронную Фармацию
russian.AppWorkingDir=Рабочая папка программы
russian.AdditionalIcons=Дополнительные ярлыки:
russian.BrandLine=ЭльФиСА — удобный заказ лекарств для аптек
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
; Avalonia self-contained выпуск (весь рантайм .NET внутри — без зависимостей у клиента).
; Сборка кладётся в ..\publish\win-x64 скриптом build-installer.ps1, exe уже переименован
; в «Электронная Фармация.exe» (совместимо с авто-обновлением старых клиентов).
Source: "..\publish\win-x64\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "assets\elfisa.ico"; DestDir: "{app}"; DestName: "elfisa.ico"; Flags: ignoreversion
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\elfisa.ico"; Comment: "{cm:BrandLine}"
Name: "{group}\Удалить {#MyAppName}"; Filename: "{uninstallexe}"; IconFilename: "{app}\elfisa.ico"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\elfisa.ico"; Comment: "{cm:BrandLine}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchAfterInstall}"; Flags: nowait postinstall skipifsilent
[UninstallDelete]
Type: filesandordirs; Name: "{app}\Logs"
Type: files; Name: "{app}\*.pdb"

View File

@ -1,24 +0,0 @@
# Установщик ЭльФиСА
## Публичная ссылка (CDN)
https://cdn.24pharmdata.ru
При открытии корня CDN сразу скачивается установщик.
Стабильное имя файла:
https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe
## Локальная сборка
- `installer/output/ElfisaPharmacy-Setup-1.0.20.exe`
- `installer/output/ElfisaPharmacy-Setup.exe` (копия для CDN)
## Пересборка
```powershell
.\scripts\build-installer.ps1
```
После сборки залейте `ElfisaPharmacy-Setup.exe` на CDN (или сделайте его index/default document).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1,18 +0,0 @@
ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ
Электронная Фармация (ЭльФиСА)
1. Программа предоставляется для использования в рамках договора
с поставщиком услуг PharmData / 24pharmdata.
2. Запрещается передавать третьим лицам учётные данные доступа,
а также модифицировать клиент способом, нарушающим безопасность
обмена данными.
3. Пользователь несёт ответственность за корректность оформляемых
заказов и за сохранность локальных данных на рабочем месте.
4. Устанавливая программу, вы подтверждаете согласие с условиями
использования сервиса и политикой обработки данных, действующими
у оператора сервиса.
При возникновении вопросов обратитесь в службу поддержки.

View File

@ -1,20 +0,0 @@
ЭльФиСА — Электронная Фармация
================================
Добро пожаловать в установку настольного клиента «Электронная Фармация».
Программа предназначена для работы аптек с прайс-листами поставщиков,
заказами, накладными и обменом данными с сервером PharmData.
Скачать установщик всегда можно по адресу:
https://cdn.24pharmdata.ru
Установщик:
• скопирует программу в выбранную папку;
• предложит ярлык в меню «Пуск»;
• по желанию создаст ярлык на рабочем столе.
Рекомендуется закрыть запущенную «Электронную Фармацию» перед установкой.
После установки при первом запуске выполните вход в систему.
© PharmData / ЭльФиСА

View File

@ -1,59 +0,0 @@
#Requires -Version 5.1
param([switch]$SkipBuild)
$ErrorActionPreference = "Stop"
$root = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $root
Write-Host "Root: $root" -ForegroundColor Cyan
$iscc = @(
"$env:LocalAppData\Programs\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
"${env:ProgramFiles}\Inno Setup 6\ISCC.exe"
) | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $iscc) {
throw "Inno Setup 6 (ISCC.exe) не найден. Установите: winget install JRSoftware.InnoSetup"
}
$pub = "publish\win-x64"
if (-not $SkipBuild) {
Write-Host "Публикация Avalonia (win-x64, self-contained)..." -ForegroundColor Cyan
if (Test-Path $pub) { Remove-Item $pub -Recurse -Force }
dotnet publish "src\Elfisa.Avalonia\Elfisa.Avalonia.csproj" -c Release -r win-x64 `
--self-contained true -p:PublishSingleFile=false -p:DebugType=none -o $pub
if ($LASTEXITCODE -ne 0) { throw "Публикация приложения завершилась с ошибкой" }
}
# Имя exe, совместимое с авто-обновлением старых WinForms-клиентов
# (батч-апдейтер перезапускает файл по имени «Электронная Фармация.exe»).
# apphost находит свой .dll независимо от имени exe, поэтому переименование безопасно.
$srcExe = Join-Path $pub "Elfisa.Avalonia.exe"
$dstExe = Join-Path $pub "Электронная Фармация.exe"
if (Test-Path $srcExe) { Move-Item $srcExe $dstExe -Force }
elseif (-not (Test-Path $dstExe)) { throw "Не найден собранный exe в $pub" }
Write-Host "Сборка установщика..." -ForegroundColor Cyan
& $iscc "installer\ElfisaPharmacy.iss"
if ($LASTEXITCODE -ne 0) { throw "Inno Setup завершился с ошибкой" }
$out = Get-ChildItem "installer\output\ElfisaPharmacy-Setup-*.exe" |
Where-Object { $_.Name -notmatch '\.bak\.' -and $_.Name -ne 'ElfisaPharmacy-Setup.exe' } |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if (-not $out) {
throw "Не найден собранный Setup exe в installer\output"
}
# Стабильное имя для CDN: https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe
$stable = Join-Path $out.DirectoryName "ElfisaPharmacy-Setup.exe"
Copy-Item $out.FullName $stable -Force
Write-Host ""
Write-Host "Готово: $($out.FullName)" -ForegroundColor Green
Write-Host "CDN-файл: $stable" -ForegroundColor Green
Write-Host ("Размер: {0:N1} МБ" -f ($out.Length / 1MB))
Write-Host ""
Write-Host "Залейте на CDN:" -ForegroundColor Cyan
Write-Host " https://cdn.24pharmdata.ru/ <- корень отдаёт этот Setup"
Write-Host " или файл: ElfisaPharmacy-Setup.exe"

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<configSections> <configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
@ -42,18 +42,6 @@
<setting name="InvoiceExportPath" serializeAs="String"> <setting name="InvoiceExportPath" serializeAs="String">
<value /> <value />
</setting> </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>
</configuration> </configuration>

View File

@ -1,14 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization;
namespace Электроннаяармация.Classes namespace Электроннаяармация.Classes
{ {
@ -157,362 +154,21 @@ namespace Электронная_Фармация.Classes
} }
} }
public async Task<List<BuyerDto>> GetBuyersAsync()
{
EnsureAuthenticated();
var responseBody = await SendAuthorizedGetAsync("/api/buyers", "загрузку покупателей");
return DeserializeBuyers(responseBody);
}
public async Task<BuyerDto> GetBuyerByIdAsync(string buyerId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(buyerId))
{
throw new ArgumentException("Не указан buyer_id.", nameof(buyerId));
}
var responseBody = await SendAuthorizedGetAsync(
$"/api/buyers/{Uri.EscapeDataString(buyerId.Trim())}",
"загрузку покупателя");
try
{
return JsonSerializer.Deserialize<BuyerDto>(responseBody, JsonOptions);
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать покупателя", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателя: {ex.Message}", ex);
}
}
public async Task<List<BuyerPriceListAssignmentDto>> GetBuyerPriceListsAsync(string buyerId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(buyerId))
{
throw new ArgumentException("Не указан buyer_id.", nameof(buyerId));
}
var responseBody = await SendAuthorizedGetAsync(
$"/api/buyers/{Uri.EscapeDataString(buyerId.Trim())}/price-lists",
"загрузку назначений прайсов покупателя");
return DeserializeBuyerPriceLists(responseBody);
}
public async Task<List<PriceListDto>> GetPriceListsAsync()
{
EnsureAuthenticated();
var responseBody = await SendAuthorizedGetAsync("/api/price-lists", "загрузку прайс-листов");
return DeserializePriceLists(responseBody);
}
public async Task<List<PriceListRegionDto>> GetPriceListRegionsAsync(string priceListId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(priceListId))
{
throw new ArgumentException("Не указан price_list_id.", nameof(priceListId));
}
var path = "/api/sc/price-list-regions?price_list_id=" + Uri.EscapeDataString(priceListId.Trim());
var responseBody = await SendAuthorizedGetAsync(path, "загрузку региональных наценок прайса");
return DeserializePriceListRegions(responseBody);
}
private static List<BuyerDto> DeserializeBuyers(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<BuyerDto>>(responseBody, JsonOptions)
?? new List<BuyerDto>();
}
var wrapped = JsonSerializer.Deserialize<BuyersResponse>(responseBody, JsonOptions);
return wrapped?.Buyers ?? wrapped?.Items ?? new List<BuyerDto>();
}
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать список покупателей", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке покупателей: {ex.Message}", ex);
}
}
private static List<BuyerPriceListAssignmentDto> DeserializeBuyerPriceLists(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<BuyerPriceListAssignmentDto>>(responseBody, JsonOptions)
?? new List<BuyerPriceListAssignmentDto>();
}
var wrapped = JsonSerializer.Deserialize<BuyerPriceListsResponse>(responseBody, JsonOptions);
return wrapped?.Items ?? wrapped?.PriceLists ?? new List<BuyerPriceListAssignmentDto>();
}
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать назначения прайсов покупателя", ex);
throw new HttpRequestException(
$"Некорректный ответ сервера при загрузке назначений прайсов: {ex.Message}",
ex);
}
}
private static List<PriceListDto> DeserializePriceLists(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<PriceListDto>>(responseBody, JsonOptions)
?? new List<PriceListDto>();
}
var wrapped = JsonSerializer.Deserialize<PriceListsResponse>(responseBody, JsonOptions);
return wrapped?.PriceLists ?? wrapped?.Items ?? new List<PriceListDto>();
}
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать прайс-листы", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке прайс-листов: {ex.Message}", ex);
}
}
private static List<PriceListRegionDto> DeserializePriceListRegions(string responseBody)
{
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<PriceListRegionDto>>(responseBody, JsonOptions)
?? new List<PriceListRegionDto>();
}
}
return new List<PriceListRegionDto>();
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать региональные наценки прайса", ex);
throw new HttpRequestException(
$"Некорректный ответ сервера при загрузке региональных наценок: {ex.Message}",
ex);
}
}
public async Task<List<InvoiceApiItem>> GetInvoicesAsync() public async Task<List<InvoiceApiItem>> GetInvoicesAsync()
{
var orders = await GetBuyerOrdersAsync(status: "Placed", limit: 200);
var result = new List<InvoiceApiItem>();
foreach (var order in orders)
{
var detail = await GetBuyerOrderByIdAsync(order.OrderID);
if (detail == null)
{
continue;
}
result.Add(InvoiceApiItem.FromBuyerOrder(detail));
}
AppDebugLog.Info("ApiClient", $"Накладные (через buyer/orders) получены: {result.Count} шт.");
return result;
}
public async Task<List<BuyerOrderListItemDto>> GetBuyerOrdersAsync(string status = null, int limit = 200, int offset = 0)
{ {
EnsureAuthenticated(); EnsureAuthenticated();
var query = new List<string>(); var responseBody = await SendAuthorizedGetAsync("/api/buyer/invoices", "загрузку накладных");
if (!string.IsNullOrWhiteSpace(status))
{
query.Add($"status={Uri.EscapeDataString(status)}");
}
if (limit > 0)
{
query.Add($"limit={limit}");
}
if (offset > 0)
{
query.Add($"offset={offset}");
}
var path = "/api/buyer/orders";
if (query.Count > 0)
{
path += "?" + string.Join("&", query);
}
var responseBody = await SendAuthorizedGetAsync(path, "загрузку заказов");
try try
{ {
var response = JsonSerializer.Deserialize<BuyerOrdersResponse>(responseBody, JsonOptions); var items = JsonSerializer.Deserialize<List<InvoiceApiItem>>(responseBody, JsonOptions) ?? new List<InvoiceApiItem>();
return response?.Orders ?? new List<BuyerOrderListItemDto>(); AppDebugLog.Info("ApiClient", $"Накладные получены: {items.Count} шт.");
return items;
} }
catch (Exception ex) catch (Exception ex)
{ {
AppDebugLog.Error("ApiClient", "Не удалось разобрать список заказов", ex); AppDebugLog.Error("ApiClient", "Не удалось разобрать ответ накладных", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке заказов: {ex.Message}", ex); throw new HttpRequestException($"Некорректный ответ сервера при загрузке накладных: {ex.Message}", ex);
}
}
/// <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()
{
EnsureAuthenticated();
var responseBody = await SendAuthorizedGetAsync("/api/buyer/locations", "загрузку грузополучателей");
try
{
using (var doc = JsonDocument.Parse(responseBody))
{
if (doc.RootElement.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<BuyerLocationDto>>(responseBody, JsonOptions)
?? new List<BuyerLocationDto>();
}
}
return new List<BuyerLocationDto>();
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", "Не удалось разобрать грузополучателей", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке грузополучателей: {ex.Message}", ex);
}
}
public async Task<BuyerOrderDetailDto> GetBuyerOrderByIdAsync(string orderId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(orderId))
{
throw new ArgumentException("Не указан ID заказа.", nameof(orderId));
}
var responseBody = await SendAuthorizedGetAsync($"/api/buyer/orders/{Uri.EscapeDataString(orderId)}", "загрузку заказа");
try
{
return JsonSerializer.Deserialize<BuyerOrderDetailDto>(responseBody, JsonOptions);
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", $"Не удалось разобрать заказ {orderId}", ex);
throw new HttpRequestException($"Некорректный ответ сервера при загрузке заказа: {ex.Message}", ex);
}
}
public async Task<BuyerOrderDetailDto> PlaceBuyerOrderAsync(string orderId)
{
EnsureAuthenticated();
if (string.IsNullOrWhiteSpace(orderId))
{
throw new ArgumentException("Не указан ID заказа.", nameof(orderId));
}
var responseBody = await SendAuthorizedPostAsync($"/api/buyer/orders/{Uri.EscapeDataString(orderId)}/place", string.Empty, "оформление заказа");
try
{
return JsonSerializer.Deserialize<BuyerOrderDetailDto>(responseBody, JsonOptions);
}
catch (Exception ex)
{
AppDebugLog.Error("ApiClient", $"Не удалось разобрать ответ оформления заказа {orderId}", ex);
throw new HttpRequestException($"Некорректный ответ сервера при оформлении заказа: {ex.Message}", ex);
} }
} }
@ -531,63 +187,6 @@ namespace Электронная_Фармация.Classes
return await SendAuthorizedGetWithProgressAsync(path, operationName, null); return await SendAuthorizedGetWithProgressAsync(path, operationName, null);
} }
private async Task<string> SendAuthorizedPostAsync(string path, string requestBody, string operationName)
{
var fullUrl = BuildFullUrl(path);
var stopwatch = Stopwatch.StartNew();
AppDebugLog.ApiRequest("POST", fullUrl, requestBody, $"token={AppDebugLog.MaskToken(_token)}");
try
{
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fullUrl))
{
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
httpRequest.Content = new StringContent(requestBody ?? string.Empty, Encoding.UTF8, "application/json");
var response = await _httpClient.SendAsync(httpRequest);
var responseBody = await response.Content.ReadAsStringAsync();
stopwatch.Stop();
AppDebugLog.ApiResponse("POST", 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);
var message = $"Сессия истекла: {error?.Error ?? "Необходимо войти заново"}";
AppDebugLog.ApiHttpError("POST", fullUrl, (int)response.StatusCode, responseBody, operationName);
throw new UnauthorizedAccessException(message);
}
var httpError = TryDeserializeError(responseBody);
var httpMessage = $"Ошибка HTTP {response.StatusCode}: {httpError?.Error ?? responseBody}";
AppDebugLog.ApiHttpError("POST", fullUrl, (int)response.StatusCode, responseBody, operationName);
throw new HttpRequestException(httpMessage);
}
}
catch (UnauthorizedAccessException)
{
throw;
}
catch (HttpRequestException ex) when (!ex.Message.StartsWith("Ошибка HTTP"))
{
stopwatch.Stop();
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
throw WrapNetworkError(ex, operationName);
}
catch (TaskCanceledException ex)
{
stopwatch.Stop();
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
throw WrapNetworkError(ex, operationName);
}
}
private async Task<string> SendAuthorizedGetWithProgressAsync( private async Task<string> SendAuthorizedGetWithProgressAsync(
string path, string path,
string operationName, string operationName,
@ -773,208 +372,6 @@ namespace Электронная_Фармация.Classes
public string ConsigneeName { get; set; } public string ConsigneeName { get; set; }
public string InvoiceSum { get; set; } public string InvoiceSum { get; set; }
public string RefuseSum { get; set; } public string RefuseSum { get; set; }
public string SourceOrderId { get; set; }
public List<InvoiceApiOrderItem> Items { get; set; } = new List<InvoiceApiOrderItem>();
public static InvoiceApiItem FromBuyerOrder(BuyerOrderDetailDto detail)
{
var supplierNames = detail.Items == null
? string.Empty
: string.Join(", ", detail.Items
.Select(i => i.Supplier)
.Where(s => !string.IsNullOrWhiteSpace(s))
.Distinct());
var invoiceDate = detail.PlacedAt ?? detail.CreatedAt;
var invoice = new InvoiceApiItem
{
InvoiceNumber = string.IsNullOrWhiteSpace(detail.LocalOrderNumber)
? detail.OrderID
: detail.LocalOrderNumber,
InvoiceDate = invoiceDate?.ToString("yyyy-MM-dd") ?? string.Empty,
SupplierName = supplierNames,
ConsigneeName = detail.LocationAddress ?? string.Empty,
InvoiceSum = (detail.TotalAmount ?? 0m).ToString(System.Globalization.CultureInfo.InvariantCulture),
RefuseSum = "0",
SourceOrderId = detail.OrderID
};
if (detail.Items != null)
{
foreach (var item in detail.Items)
{
invoice.Items.Add(new InvoiceApiOrderItem
{
GoodCode = item.ItemCode ?? string.Empty,
GoodName = item.Name ?? string.Empty,
SupplierName = item.Supplier ?? string.Empty,
Qty = item.Qty,
Price = item.UnitPrice,
Total = item.Total
});
}
}
return invoice;
}
}
public sealed class InvoiceApiOrderItem
{
public string GoodCode { get; set; }
public string GoodName { get; set; }
public string SupplierName { get; set; }
public decimal Qty { get; set; }
public decimal Price { get; set; }
public decimal Total { get; set; }
}
public sealed class BuyerOrdersResponse
{
[JsonPropertyName("orders")]
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
{
[JsonPropertyName("buyer_location_id")]
public string BuyerLocationID { get; set; }
[JsonPropertyName("buyer_id")]
public string BuyerID { get; set; }
[JsonPropertyName("address")]
public string Address { get; set; }
[JsonPropertyName("region_id")]
public string RegionID { get; set; }
[JsonPropertyName("is_default")]
public bool IsDefault { get; set; }
[JsonPropertyName("region_name")]
public string RegionName { get; set; }
}
public sealed class BuyerOrderListItemDto
{
[JsonPropertyName("order_id")]
public string OrderID { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
}
public sealed class BuyerOrderDetailDto
{
[JsonPropertyName("order_id")]
public string OrderID { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("total_amount")]
public decimal? TotalAmount { get; set; }
[JsonPropertyName("created_at")]
public DateTime? CreatedAt { get; set; }
[JsonPropertyName("placed_at")]
public DateTime? PlacedAt { get; set; }
[JsonPropertyName("comment")]
public string Comment { get; set; }
[JsonPropertyName("location_address")]
public string LocationAddress { get; set; }
[JsonPropertyName("items")]
public List<BuyerOrderItemDetailDto> Items { get; set; }
[JsonIgnore]
public string LocalOrderNumber { get; set; }
}
public sealed class BuyerOrderItemDetailDto
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("supplier")]
public string Supplier { get; set; }
[JsonPropertyName("qty")]
public decimal Qty { get; set; }
[JsonPropertyName("unit_price")]
public decimal UnitPrice { get; set; }
[JsonPropertyName("total")]
public decimal Total { get; set; }
[JsonPropertyName("item_code")]
public string ItemCode { get; set; }
} }
public sealed class HttpTransferProgress public sealed class HttpTransferProgress

View File

@ -10,72 +10,12 @@ namespace Электронная_Фармация.Classes
public static class AppConfig public static class AppConfig
{ {
public const string DefaultApiBaseUrl = "https://24pharmdata.ru"; public const string DefaultApiBaseUrl = "https://24pharmdata.ru";
public const string DefaultUpdateCheckBaseUrl = "https://git.24pharmdata.ru";
public const string DefaultUpdateRepo = "pharmdata/elfisa-pharmacy";
/// <summary>Публичная раздача установщика (корень CDN отдаёт Setup.exe).</summary>
public const string DefaultInstallerCdnUrl = "https://cdn.24pharmdata.ru";
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 readonly object _sqliteInteropLock = new object();
public static string InstallerDownloadUrl
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("ELF_INSTALLER_URL");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
return fromEnv.Trim();
}
return DefaultInstallerDownloadUrl;
}
}
public static string InstallerCdnUrl
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("ELF_CDN_URL");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
return fromEnv.Trim().TrimEnd('/');
}
return DefaultInstallerCdnUrl;
}
}
/// <summary> /// <summary>
/// Replaces known dead API URLs in saved user settings. /// Replaces known dead API URLs in saved user settings.
/// </summary> /// </summary>
/// <summary>
/// Carries user settings across AssemblyVersion bumps (new user.config folder).
/// </summary>
public static void UpgradeUserSettingsIfNeeded()
{
try
{
if (Settings.Default.UpgradeSettings)
{
Settings.Default.Upgrade();
Settings.Default.UpgradeSettings = false;
Settings.Default.Save();
AppDebugLog.Info("AppConfig", "User settings upgraded from previous version");
}
}
catch (Exception ex)
{
AppDebugLog.Info("AppConfig", $"Upgrade settings skipped: {ex.Message}");
}
}
public static void MigrateLegacySettings() public static void MigrateLegacySettings()
{ {
var current = Settings.Default.ApiBaseUrl; var current = Settings.Default.ApiBaseUrl;
@ -115,21 +55,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))
@ -147,87 +72,30 @@ namespace Электронная_Фармация.Classes
return url; return url;
} }
/// <summary>
/// Writable per-user data (DB, logs). Program Files is read-only for normal users.
/// </summary>
public static string UserDataDirectory
{
get
{
var dir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ЭльФиСА",
"Электронная Фармация");
try
{
Directory.CreateDirectory(dir);
}
catch
{
// Caller will surface a clearer error when opening the DB.
}
return dir;
}
}
public static string LogsDirectory => Path.Combine(UserDataDirectory, "Logs");
public static string SqliteDbPath public static string SqliteDbPath
{ {
get get
{ {
MigrateLegacySqliteDatabaseIfNeeded();
return Path.Combine(UserDataDirectory, "efClient.db");
}
}
/// <summary>
/// One-time copy from install folder (old portable layout) into LocalAppData.
/// </summary>
private static void MigrateLegacySqliteDatabaseIfNeeded()
{
try
{
var target = Path.Combine(UserDataDirectory, "efClient.db");
if (File.Exists(target))
{
return;
}
var baseDir = AppDomain.CurrentDomain.BaseDirectory; var baseDir = AppDomain.CurrentDomain.BaseDirectory;
var candidates = new[] var root = Path.Combine(baseDir, "efClient.db");
var data = Path.Combine(baseDir, "Data", "efClient.db");
if (File.Exists(root))
{ {
Path.Combine(baseDir, "efClient.db"), return root;
Path.Combine(baseDir, "Data", "efClient.db")
};
foreach (var source in candidates)
{
if (!File.Exists(source))
{
continue;
} }
File.Copy(source, target, overwrite: false); if (File.Exists(data))
AppDebugLog.Info("AppConfig", $"SQLite DB migrated: {source} -> {target}");
return;
}
}
catch (Exception ex)
{ {
AppDebugLog.Info("AppConfig", $"SQLite DB migrate skipped: {ex.Message}"); return data;
}
return root;
} }
} }
public static string SqliteConnectionString public static string SqliteConnectionString
{ {
get get { return $"Data Source={SqliteDbPath};Version=3;New=False;"; }
{
EnsureSQLiteInteropInBaseDir();
return $"Data Source={SqliteDbPath};Version=3;New=False;";
}
} }
public static string LocationId public static string LocationId
@ -251,113 +119,5 @@ namespace Электронная_Фармация.Classes
Settings.Default.InvoiceExportPath = (path ?? string.Empty).Trim(); Settings.Default.InvoiceExportPath = (path ?? string.Empty).Trim();
Settings.Default.Save(); Settings.Default.Save();
} }
/// <summary>
/// Gitea base URL for forced update checks (no trailing slash).
/// </summary>
public static string UpdateCheckBaseUrl
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("ELF_UPDATE_CHECK_BASE_URL");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
return NormalizeApiBaseUrl(fromEnv);
}
var fromSettings = Settings.Default.UpdateCheckBaseUrl;
if (!string.IsNullOrWhiteSpace(fromSettings))
{
return NormalizeApiBaseUrl(fromSettings);
}
return DefaultUpdateCheckBaseUrl;
}
}
/// <summary>
/// owner/repo on Gitea, e.g. pharmdata/elfisa-pharmacy.
/// </summary>
public static string UpdateRepo
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("ELF_UPDATE_REPO");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
return fromEnv.Trim().Trim('/');
}
var fromSettings = Settings.Default.UpdateRepo;
if (!string.IsNullOrWhiteSpace(fromSettings))
{
return fromSettings.Trim().Trim('/');
}
return DefaultUpdateRepo;
}
}
/// <summary>
/// Optional read-only Gitea token for private release repos.
/// </summary>
public static string UpdateApiToken
{
get
{
var fromEnv = Environment.GetEnvironmentVariable("ELF_UPDATE_API_TOKEN");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
return fromEnv.Trim();
}
return Settings.Default.UpdateApiToken ?? string.Empty;
}
}
private static void EnsureSQLiteInteropInBaseDir()
{
// System.Data.SQLite ожидает SQLite.Interop.dll рядом с exe.
// Важно: разрядность DLL должна совпадать с процессом (Prefer32Bit => x86).
if (_sqliteInteropPrepared)
{
return;
}
lock (_sqliteInteropLock)
{
if (_sqliteInteropPrepared)
{
return;
}
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
var targetDll = Path.Combine(baseDir, "SQLite.Interop.dll");
var archFolder = Environment.Is64BitProcess ? "x64" : "x86";
var sourceDll = Path.Combine(baseDir, archFolder, "SQLite.Interop.dll");
var sourcePdb = Path.Combine(baseDir, archFolder, "SQLite.Interop.pdb");
try
{
if (File.Exists(sourceDll))
{
// Всегда подкладываем DLL нужной разрядности (даже если в корне уже лежит "чужая").
File.Copy(sourceDll, targetDll, overwrite: true);
var targetPdb = Path.Combine(baseDir, "SQLite.Interop.pdb");
if (File.Exists(sourcePdb))
{
File.Copy(sourcePdb, targetPdb, overwrite: true);
}
}
}
catch
{
// Если копирование не удалось (права/readonly/и т.п.), дальше будет явное исключение от SQLite.
}
_sqliteInteropPrepared = true;
}
}
} }
} }

View File

@ -8,7 +8,7 @@ namespace Электронная_Фармация.Classes
{ {
/// <summary> /// <summary>
/// File logger for API/auth/network debugging. /// File logger for API/auth/network debugging.
/// Logs are written to %LocalAppData%\ЭльФиСА\...\Logs\api-debug-YYYY-MM-DD.log. /// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable.
/// </summary> /// </summary>
public static class AppDebugLog public static class AppDebugLog
{ {
@ -33,7 +33,8 @@ namespace Электронная_Фармация.Classes
} }
} }
public static string LogDirectory => AppConfig.LogsDirectory; public static string LogDirectory =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
public static string CurrentLogFilePath => public static string CurrentLogFilePath =>
Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log"); Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log");

View File

@ -1,287 +0,0 @@
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
namespace Электроннаяармация.Classes
{
/// <summary>
/// Downloads a Gitea release ZIP and applies it via an external batch updater
/// so the running exe can be replaced. Preserves local databases and Logs.
/// </summary>
public static class AutoUpdater
{
public static async Task ApplyUpdateAsync(
UpdateCheckResult update,
IProgress<string> status = null)
{
if (update == null)
{
throw new ArgumentNullException(nameof(update));
}
var downloadUrl = !string.IsNullOrWhiteSpace(update.DownloadUrl)
? update.DownloadUrl
: update.ReleaseUrl;
if (string.IsNullOrWhiteSpace(downloadUrl) ||
!downloadUrl.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
if (!string.IsNullOrWhiteSpace(downloadUrl) &&
!downloadUrl.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(
"В релизе нет ZIP-сборки. Прикрепите ElectronicPharmacy-vX.Y.Z.zip к Release.");
}
throw new InvalidOperationException("Нет ссылки на ZIP обновления.");
}
var installDir = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var currentExe = Process.GetCurrentProcess().MainModule?.FileName;
if (string.IsNullOrWhiteSpace(currentExe) || !File.Exists(currentExe))
{
currentExe = Directory.GetFiles(installDir, "*.exe")
.FirstOrDefault(p => !p.EndsWith(".vshost.exe", StringComparison.OrdinalIgnoreCase));
}
if (string.IsNullOrWhiteSpace(currentExe))
{
throw new InvalidOperationException("Не найден текущий exe.");
}
var workRoot = Path.Combine(Path.GetTempPath(), "elfisa-update-" + Guid.NewGuid().ToString("N"));
var zipPath = Path.Combine(workRoot, "update.zip");
var extractDir = Path.Combine(workRoot, "extract");
Directory.CreateDirectory(extractDir);
try
{
Report(status, "Скачивание обновления...");
await DownloadAsync(downloadUrl, zipPath).ConfigureAwait(false);
Report(status, "Распаковка...");
ZipFile.ExtractToDirectory(zipPath, extractDir);
var payloadDir = FindPayloadDirectory(extractDir);
if (payloadDir == null)
{
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 updaterBat = Path.Combine(workRoot, "apply-update.cmd");
WriteUpdaterScript(updaterBat);
Report(status, needsElevation
? "Нужны права администратора для установки в Program Files..."
: "Подготовка перезапуска...");
var batchArgs = Quote(payloadDir) + " " + Quote(installDir) + " " +
Quote(Path.GetFileName(currentExe)) + " " +
Process.GetCurrentProcess().Id;
var psi = new ProcessStartInfo
{
UseShellExecute = true,
WorkingDirectory = workRoot,
WindowStyle = needsElevation ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden,
CreateNoWindow = !needsElevation
};
if (needsElevation)
{
// ВАЖНО: повышаем права через cmd.exe (PE-исполняемый), а НЕ через
// .cmd-файл напрямую. ShellExecute с verb=runas на .cmd срабатывает не
// на всех системах (у batch может не быть runas-ассоциации), из-за чего
// elevated-процесс молча не запускается и обновление не применяется.
psi.FileName = "cmd.exe";
psi.Arguments = "/c \"\"" + updaterBat + "\" " + batchArgs + "\"";
psi.Verb = "runas";
}
else
{
psi.FileName = updaterBat;
psi.Arguments = batchArgs;
}
try
{
Process.Start(psi);
}
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// ERROR_CANCELLED — пользователь отклонил UAC
throw new InvalidOperationException(
"Обновление отменено: нужны права администратора " +
"(программа установлена в Program Files).",
ex);
}
AppDebugLog.Info("AutoUpdater",
$"Updater started. elevate={needsElevation}, payload={payloadDir}, install={installDir}, ver={update.LatestVersion}");
}
catch
{
try
{
if (Directory.Exists(workRoot))
{
Directory.Delete(workRoot, true);
}
}
catch
{
// ignore cleanup errors
}
throw;
}
}
private static bool IsDirectoryWritable(string directory)
{
try
{
var probe = Path.Combine(directory, ".elfisa-write-" + Guid.NewGuid().ToString("N"));
File.WriteAllText(probe, "ok");
File.Delete(probe);
return true;
}
catch
{
return false;
}
}
private static async Task DownloadAsync(string url, string zipPath)
{
using (var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) })
{
var token = AppConfig.UpdateApiToken;
if (!string.IsNullOrWhiteSpace(token))
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("token", token.Trim());
}
using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)
.ConfigureAwait(false))
{
response.EnsureSuccessStatusCode();
using (var fs = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fs).ConfigureAwait(false);
}
}
}
}
private static string FindPayloadDirectory(string extractRoot)
{
// ZIP may contain a single root folder (Compress-Archive of a directory).
var exe = Directory.GetFiles(extractRoot, "*.exe", SearchOption.AllDirectories)
.FirstOrDefault(p =>
{
var name = Path.GetFileName(p) ?? string.Empty;
return !name.EndsWith(".vshost.exe", StringComparison.OrdinalIgnoreCase) &&
!name.Equals("UpdateCheckSmoke.exe", StringComparison.OrdinalIgnoreCase) &&
!name.StartsWith("unins", StringComparison.OrdinalIgnoreCase);
});
return exe == null ? null : Path.GetDirectoryName(exe);
}
private static void WriteUpdaterScript(string path)
{
// Waits for app exit, force-kills leftovers, renames locked exe,
// copies with robocopy, verifies, restarts. Log: %TEMP%\elfisa-updater.log
var sb = new StringBuilder();
sb.AppendLine("@echo off");
sb.AppendLine("setlocal EnableDelayedExpansion");
sb.AppendLine("set \"SRC=%~1\"");
sb.AppendLine("set \"DST=%~2\"");
sb.AppendLine("set \"EXENAME=%~3\"");
sb.AppendLine("set \"PID=%~4\"");
sb.AppendLine("set \"LOG=%TEMP%\\elfisa-updater.log\"");
sb.AppendLine("echo ===== %DATE% %TIME% ===== > \"%LOG%\"");
sb.AppendLine("echo SRC=%SRC%>> \"%LOG%\"");
sb.AppendLine("echo DST=%DST%>> \"%LOG%\"");
sb.AppendLine("echo EXE=%EXENAME% PID=%PID%>> \"%LOG%\"");
sb.AppendLine("set \"N=0\"");
sb.AppendLine("echo Waiting for PID %PID% ... >> \"%LOG%\"");
sb.AppendLine(":waitloop");
sb.AppendLine("tasklist /FI \"PID eq %PID%\" 2>nul | findstr /C:\" %PID% \" >nul");
sb.AppendLine("if errorlevel 1 goto donewait");
sb.AppendLine("set /a N+=1");
sb.AppendLine("if !N! GEQ 40 (");
sb.AppendLine(" echo force kill PID %PID% >> \"%LOG%\"");
sb.AppendLine(" taskkill /PID %PID% /F >nul 2>&1");
sb.AppendLine(" goto donewait");
sb.AppendLine(")");
sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
sb.AppendLine("goto waitloop");
sb.AppendLine(":donewait");
sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
// Kill any leftover instances by image name (locks Program Files exe).
sb.AppendLine("echo taskkill by name >> \"%LOG%\"");
sb.AppendLine("taskkill /F /IM \"%EXENAME%\" >nul 2>&1");
sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
// Rename locked/old exe so copy can proceed even if handle lingered.
sb.AppendLine("if exist \"%DST%\\%EXENAME%.old\" del /F /Q \"%DST%\\%EXENAME%.old\" >nul 2>&1");
sb.AppendLine("if exist \"%DST%\\%EXENAME%\" (");
sb.AppendLine(" move /Y \"%DST%\\%EXENAME%\" \"%DST%\\%EXENAME%.old\" >> \"%LOG%\" 2>&1");
sb.AppendLine(")");
sb.AppendLine("echo Copying update... >> \"%LOG%\"");
// /IS /IT — force overwrite even if timestamps look the same
sb.AppendLine("robocopy \"%SRC%\" \"%DST%\" /E /IS /IT /R:5 /W:1 /NFL /NDL /NJH /NJS /XF efClient.db *.db /XD Logs");
sb.AppendLine("set \"RC=!ERRORLEVEL!\"");
sb.AppendLine("echo robocopy=!RC! >> \"%LOG%\"");
sb.AppendLine("if not exist \"%DST%\\%EXENAME%\" (");
sb.AppendLine(" echo COPY FAILED — restoring old exe >> \"%LOG%\"");
sb.AppendLine(" if exist \"%DST%\\%EXENAME%.old\" move /Y \"%DST%\\%EXENAME%.old\" \"%DST%\\%EXENAME%\" >nul 2>&1");
sb.AppendLine(" echo Не удалось заменить файлы программы. См. %TEMP%\\elfisa-updater.log");
sb.AppendLine(" pause");
sb.AppendLine(" exit /b 1");
sb.AppendLine(")");
sb.AppendLine("if exist \"%DST%\\%EXENAME%.old\" del /F /Q \"%DST%\\%EXENAME%.old\" >nul 2>&1");
sb.AppendLine("echo Starting \"%DST%\\%EXENAME%\" >> \"%LOG%\"");
sb.AppendLine("start \"\" /D \"%DST%\" \"%DST%\\%EXENAME%\"");
sb.AppendLine("exit /b 0");
File.WriteAllText(path, sb.ToString(), Encoding.Default);
}
private static string Quote(string value)
{
return "\"" + (value ?? string.Empty).Replace("\"", string.Empty) + "\"";
}
private static void Report(IProgress<string> status, string message)
{
AppDebugLog.Info("AutoUpdater", message);
status?.Report(message);
}
}
}

View File

@ -1,137 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Электроннаяармация.Classes
{
public sealed class BuyerDto
{
[JsonPropertyName("buyer_id")]
public string BuyerId { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("login")]
public string Login { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
public string ResolvedId =>
!string.IsNullOrWhiteSpace(BuyerId) ? BuyerId.Trim() :
!string.IsNullOrWhiteSpace(Id) ? Id.Trim() :
string.Empty;
}
public sealed class BuyersResponse
{
[JsonPropertyName("buyers")]
public List<BuyerDto> Buyers { get; set; }
[JsonPropertyName("items")]
public List<BuyerDto> Items { get; set; }
}
public sealed class BuyerPriceListAssignmentDto
{
[JsonPropertyName("price_list_id")]
public string PriceListId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("supplier_id")]
public string SupplierId { get; set; }
[JsonPropertyName("supplier_name")]
public string SupplierName { get; set; }
/// <summary>Клиентская скидка на этот прайс.</summary>
[JsonPropertyName("markup_pct")]
public decimal? MarkupPct { get; set; }
/// <summary>Базовая скидка самого прайса.</summary>
[JsonPropertyName("default_markup_pct")]
public decimal? DefaultMarkupPct { get; set; }
}
public sealed class BuyerPriceListsResponse
{
[JsonPropertyName("items")]
public List<BuyerPriceListAssignmentDto> Items { get; set; }
[JsonPropertyName("price_lists")]
public List<BuyerPriceListAssignmentDto> PriceLists { get; set; }
}
public sealed class PriceListDto
{
[JsonPropertyName("price_list_id")]
public string PriceListId { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("supplier_id")]
public string SupplierId { get; set; }
[JsonPropertyName("supplier_name")]
public string SupplierName { get; set; }
[JsonPropertyName("default_markup_pct")]
public decimal? DefaultMarkupPct { get; set; }
public string ResolvedId =>
!string.IsNullOrWhiteSpace(PriceListId) ? PriceListId.Trim() :
!string.IsNullOrWhiteSpace(Id) ? Id.Trim() :
string.Empty;
}
public sealed class PriceListsResponse
{
[JsonPropertyName("price_lists")]
public List<PriceListDto> PriceLists { get; set; }
[JsonPropertyName("items")]
public List<PriceListDto> Items { get; set; }
}
public sealed class PriceListRegionDto
{
[JsonPropertyName("region_id")]
public string RegionId { get; set; }
[JsonPropertyName("region_name")]
public string RegionName { get; set; }
[JsonPropertyName("in_work")]
public bool? InWork { get; set; }
[JsonPropertyName("markup_pct")]
public decimal? MarkupPct { get; set; }
[JsonPropertyName("base_price_type")]
public string BasePriceType { get; set; }
}
/// <summary>
/// Скидки, доступные для одной позиции прайса.
/// Отсутствующий источник = 0.
/// </summary>
public sealed class ItemDiscountParts
{
public string PriceListId { get; set; }
public decimal PriceListDiscountPct { get; set; }
public decimal RegionDiscountPct { get; set; }
public decimal ClientDiscountPct { get; set; }
public decimal TotalDiscountPct =>
PriceListDiscountPct + RegionDiscountPct + ClientDiscountPct;
}
}

View File

@ -1,506 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data.SQLite;
namespace Электроннаяармация.Classes
{
/// <summary>
/// Аптека = грузополучатель + LocationId. Прайс общий, заказы уходят с location_id выбранной аптеки.
/// </summary>
public static class ConsigneeHelper
{
public static string GetDefaultConsigneeName()
{
try
{
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"SELECT ConsigneesName
FROM Consignees
ORDER BY ConsigneesUseAsDefault DESC, ConsigneesName
LIMIT 1",
connection))
{
var result = command.ExecuteScalar();
return result == null || result == DBNull.Value
? string.Empty
: result.ToString().Trim();
}
}
}
catch
{
return string.Empty;
}
}
public static string GetLocationIdByName(string consigneeName)
{
if (string.IsNullOrWhiteSpace(consigneeName))
{
return string.Empty;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"SELECT ifnull(LocationId, '')
FROM Consignees
WHERE ConsigneesName = @name
LIMIT 1",
connection))
{
command.Parameters.AddWithValue("@name", consigneeName.Trim());
var result = command.ExecuteScalar();
return result == null || result == DBNull.Value
? string.Empty
: result.ToString().Trim();
}
}
}
public static string ResolveLocationId(string consigneeName)
{
var fromConsignee = GetLocationIdByName(consigneeName);
if (!string.IsNullOrWhiteSpace(fromConsignee))
{
return fromConsignee;
}
return AppConfig.LocationId ?? string.Empty;
}
/// <summary>
/// Помечает аптеку активной и синхронизирует AppConfig.LocationId.
/// </summary>
public static void SetActiveConsignee(string consigneeName)
{
if (string.IsNullOrWhiteSpace(consigneeName))
{
return;
}
var name = consigneeName.Trim();
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var tx = connection.BeginTransaction())
{
using (var clear = new SQLiteCommand(
"UPDATE Consignees SET ConsigneesUseAsDefault = 0",
connection,
tx))
{
clear.ExecuteNonQuery();
}
using (var setDefault = new SQLiteCommand(
@"UPDATE Consignees
SET ConsigneesUseAsDefault = 1
WHERE ConsigneesName = @name",
connection,
tx))
{
setDefault.Parameters.AddWithValue("@name", name);
setDefault.ExecuteNonQuery();
}
tx.Commit();
}
var locationId = GetLocationIdByName(name);
if (!string.IsNullOrWhiteSpace(locationId))
{
AppConfig.SetLocationId(locationId);
}
}
}
public static void SetLocationId(string consigneeName, string locationId)
{
if (string.IsNullOrWhiteSpace(consigneeName))
{
return;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"UPDATE Consignees
SET LocationId = @loc
WHERE ConsigneesName = @name",
connection))
{
command.Parameters.AddWithValue("@loc", (locationId ?? string.Empty).Trim());
command.Parameters.AddWithValue("@name", consigneeName.Trim());
command.ExecuteNonQuery();
}
}
}
public static void MigrateGlobalLocationIdIfNeeded(SQLiteConnection connection)
{
var global = AppConfig.LocationId;
if (string.IsNullOrWhiteSpace(global))
{
return;
}
using (var command = new SQLiteCommand(
@"UPDATE Consignees
SET LocationId = @loc
WHERE (LocationId IS NULL OR trim(LocationId) = '')
AND (
ConsigneesUseAsDefault = 1
OR (SELECT COUNT(*) FROM Consignees) = 1
)",
connection))
{
command.Parameters.AddWithValue("@loc", global.Trim());
command.ExecuteNonQuery();
}
}
public static void ApplyLocationIdToEmptyConsignees(string locationId)
{
if (string.IsNullOrWhiteSpace(locationId))
{
return;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"UPDATE Consignees
SET LocationId = @loc
WHERE LocationId IS NULL OR trim(LocationId) = ''",
connection))
{
command.Parameters.AddWithValue("@loc", locationId.Trim());
command.ExecuteNonQuery();
}
}
}
public static IList<string> GetKnownSupplierIds()
{
var result = new List<string>();
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"SELECT DISTINCT trim(supplier_id)
FROM PriceList
WHERE supplier_id IS NOT NULL AND trim(supplier_id) <> ''
ORDER BY 1",
connection))
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
var id = reader.GetString(0);
if (!string.IsNullOrWhiteSpace(id))
{
result.Add(id);
}
}
}
}
return result;
}
/// <summary>
/// Процент наценки, который клиент задал вручную для аптеки.
/// null = ещё не сохраняли, брать fallback.
/// </summary>
public static decimal? GetClientMarkupPercent(string consigneeName)
{
if (string.IsNullOrWhiteSpace(consigneeName))
{
return null;
}
try
{
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"SELECT ClientMarkupPercent
FROM Consignees
WHERE ConsigneesName = @name
LIMIT 1",
connection))
{
command.Parameters.AddWithValue("@name", consigneeName.Trim());
var result = command.ExecuteScalar();
if (result == null || result == DBNull.Value)
{
return null;
}
if (result is decimal d)
{
return d;
}
if (decimal.TryParse(
Convert.ToString(result, System.Globalization.CultureInfo.InvariantCulture),
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out var parsed))
{
return parsed;
}
return null;
}
}
}
catch
{
return null;
}
}
/// <summary>
/// Сохраняет процент наценки клиента для конкретной аптеки.
/// Не сбрасывается при обновлении прайса.
/// </summary>
public static void SetClientMarkupPercent(string consigneeName, decimal markupPercent)
{
if (string.IsNullOrWhiteSpace(consigneeName))
{
return;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"UPDATE Consignees
SET ClientMarkupPercent = @pct
WHERE ConsigneesName = @name",
connection))
{
command.Parameters.AddWithValue("@pct", markupPercent);
command.Parameters.AddWithValue("@name", consigneeName.Trim());
command.ExecuteNonQuery();
}
}
}
/// <summary>
/// Если у аптеки ещё нет сохранённого процента — записать стартовое значение с сервера.
/// Уже введённые клиентом значения не трогаем.
/// </summary>
public static void SeedClientMarkupPercentIfEmpty(decimal? serverMarkupPercent)
{
if (!serverMarkupPercent.HasValue)
{
return;
}
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var command = new SQLiteCommand(
@"UPDATE Consignees
SET ClientMarkupPercent = @pct
WHERE ClientMarkupPercent IS NULL",
connection))
{
command.Parameters.AddWithValue("@pct", serverMarkupPercent.Value);
command.ExecuteNonQuery();
}
}
}
/// <summary>
/// Подтягивает грузополучателей с сервера (BuyerLocation) в локальную таблицу Consignees.
/// Источник истины — веб-кабинет; локально только кэш + выбор активной аптеки.
/// </summary>
public static int SyncFromServer(IList<BuyerLocationDto> locations)
{
if (locations == null || locations.Count == 0)
{
return 0;
}
var upserted = 0;
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var tx = connection.BeginTransaction())
{
// Сбрасываем default — выставим по данным сервера.
using (var clearDefault = new SQLiteCommand(
"UPDATE Consignees SET ConsigneesUseAsDefault = 0",
connection,
tx))
{
clearDefault.ExecuteNonQuery();
}
int nextCode;
using (var nextCodeCmd = new SQLiteCommand(
"SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees",
connection,
tx))
{
nextCode = Convert.ToInt32(nextCodeCmd.ExecuteScalar());
}
foreach (var loc in locations)
{
var locationId = (loc.BuyerLocationID ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(locationId))
{
continue;
}
var address = (loc.Address ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(address))
{
address = "Грузополучатель";
}
var name = address;
if (name.Length > 120)
{
name = name.Substring(0, 120);
}
long? existingId = null;
using (var find = new SQLiteCommand(
@"SELECT idConsignees FROM Consignees
WHERE lower(trim(LocationId)) = lower(@loc)
LIMIT 1",
connection,
tx))
{
find.Parameters.AddWithValue("@loc", locationId);
var found = find.ExecuteScalar();
if (found != null && found != DBNull.Value)
{
existingId = Convert.ToInt64(found);
}
}
if (existingId.HasValue)
{
using (var upd = new SQLiteCommand(
@"UPDATE Consignees
SET ConsigneesName = @name,
ConsigneesAddress = @address,
ConsigneesUseAsDefault = @isDefault,
LocationId = @loc
WHERE idConsignees = @id",
connection,
tx))
{
upd.Parameters.AddWithValue("@name", name);
upd.Parameters.AddWithValue("@address", address);
upd.Parameters.AddWithValue("@isDefault", loc.IsDefault ? 1 : 0);
upd.Parameters.AddWithValue("@loc", locationId);
upd.Parameters.AddWithValue("@id", existingId.Value);
upd.ExecuteNonQuery();
}
}
else
{
using (var ins = new SQLiteCommand(
@"INSERT INTO Consignees
(codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId)
VALUES (@code, @name, @address, @isDefault, @loc)",
connection,
tx))
{
ins.Parameters.AddWithValue("@code", nextCode++);
ins.Parameters.AddWithValue("@name", name);
ins.Parameters.AddWithValue("@address", address);
ins.Parameters.AddWithValue("@isDefault", loc.IsDefault ? 1 : 0);
ins.Parameters.AddWithValue("@loc", locationId);
ins.ExecuteNonQuery();
}
}
upserted++;
}
tx.Commit();
}
}
var active = GetDefaultConsigneeName();
if (!string.IsNullOrWhiteSpace(active))
{
SetActiveConsignee(active);
}
return upserted;
}
/// <summary>
/// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки.
/// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется
/// только для текущего активного просмотра.
/// </summary>
public static void SyncCartToPriceList(string consigneeName)
{
var consignee = (consigneeName ?? string.Empty).Trim().Replace("'", "''");
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var tx = connection.BeginTransaction())
{
using (var clear = new SQLiteCommand(
@"UPDATE [PriceList]
SET [Zakaz] = NULL, [SummaZakaza] = NULL;", connection, tx))
{
clear.ExecuteNonQuery();
}
using (var sync = new SQLiteCommand(
@"UPDATE [PriceList]
SET
[Zakaz] = (
SELECT t.[Zakaz]
FROM [TempOrderItems] t
WHERE t.[ConsigneeName] = @consignee
AND t.[id_PriceList_Item] = [PriceList].[id_PriceList_Item]
LIMIT 1
),
[SummaZakaza] = (
SELECT t.[SummaZakaza]
FROM [TempOrderItems] t
WHERE t.[ConsigneeName] = @consignee
AND t.[id_PriceList_Item] = [PriceList].[id_PriceList_Item]
LIMIT 1
)
WHERE EXISTS (
SELECT 1
FROM [TempOrderItems] t
WHERE t.[ConsigneeName] = @consignee
AND t.[id_PriceList_Item] = [PriceList].[id_PriceList_Item]
);",
connection,
tx))
{
sync.Parameters.AddWithValue("@consignee", consignee);
sync.ExecuteNonQuery();
}
tx.Commit();
}
}
}
}
}

View File

@ -35,6 +35,20 @@ namespace Электронная_Фармация.Classes
return; return;
} }
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
if (!HF_LocationId.PromptAndSave(Form.ActiveForm)
|| string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
AppDebugLog.Error("DataSender", "Отправка заказов: location_id не указан");
ToastNotification.ShowCustom(
"Не указан location_id. Заполните его после авторизации.",
Color.DarkOrange,
Color.White);
return;
}
}
List<BuyerOrderRequest> orders; List<BuyerOrderRequest> orders;
try try
{ {
@ -52,11 +66,6 @@ namespace Электронная_Фармация.Classes
return; return;
} }
if (!EnsureOrdersHaveLocationId(orders))
{
return;
}
int successCount = 0; int successCount = 0;
int failCount = 0; int failCount = 0;
@ -101,6 +110,20 @@ namespace Электронная_Фармация.Classes
return false; return false;
} }
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
if (!HF_LocationId.PromptAndSave(Form.ActiveForm)
|| string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: location_id не указан");
ToastNotification.ShowCustom(
"Не указан location_id. Заполните его после авторизации.",
Color.DarkOrange,
Color.White);
return false;
}
}
List<BuyerOrderRequest> orders; List<BuyerOrderRequest> orders;
try try
{ {
@ -118,11 +141,6 @@ namespace Электронная_Фармация.Classes
return false; return false;
} }
if (!EnsureOrdersHaveLocationId(orders))
{
return false;
}
bool ok = await SendOrderAsync(orders[0]); bool ok = await SendOrderAsync(orders[0]);
if (ok) if (ok)
{ {
@ -135,7 +153,7 @@ namespace Электронная_Фармация.Classes
public List<BuyerOrderRequest> FetchDataFromSql(string onlyOrderId) public List<BuyerOrderRequest> FetchDataFromSql(string onlyOrderId)
{ {
var orders = new Dictionary<string, BuyerOrderRequest>(); var orders = new Dictionary<string, BuyerOrderRequest>();
var locationCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); string locationId = AppConfig.LocationId;
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{ {
@ -143,8 +161,6 @@ namespace Электронная_Фармация.Classes
string query = @" string query = @"
SELECT SELECT
Orders.Id_Order, Orders.Id_Order,
Orders.OrderNumber,
Orders.ConsigneeName,
OrderItems.supplier_price_id, OrderItems.supplier_price_id,
OrderItems.DrugName, OrderItems.DrugName,
OrderItems.es_code, OrderItems.es_code,
@ -173,12 +189,6 @@ WHERE Orders.OrderState = 'НОВЫЙ'
while (reader.Read()) while (reader.Read())
{ {
string orderId = reader["Id_Order"].ToString().Trim(); string orderId = reader["Id_Order"].ToString().Trim();
string orderNumber = reader["OrderNumber"] == DBNull.Value
? string.Empty
: reader["OrderNumber"].ToString().Trim();
string consigneeName = reader["ConsigneeName"] == DBNull.Value
? string.Empty
: reader["ConsigneeName"].ToString().Trim();
string supplierPriceId = reader["supplier_price_id"].ToString().Trim(); string supplierPriceId = reader["supplier_price_id"].ToString().Trim();
string drugName = reader["DrugName"] == DBNull.Value ? null : reader["DrugName"].ToString().Trim(); string drugName = reader["DrugName"] == DBNull.Value ? null : reader["DrugName"].ToString().Trim();
string esCode = reader["es_code"] == DBNull.Value ? null : reader["es_code"].ToString().Trim(); string esCode = reader["es_code"] == DBNull.Value ? null : reader["es_code"].ToString().Trim();
@ -196,13 +206,9 @@ WHERE Orders.OrderState = 'НОВЫЙ'
if (!orders.TryGetValue(orderId, out BuyerOrderRequest order)) if (!orders.TryGetValue(orderId, out BuyerOrderRequest order))
{ {
string locationId = ResolveLocationIdCached(consigneeName, locationCache);
order = new BuyerOrderRequest order = new BuyerOrderRequest
{ {
LocalOrderId = orderId, LocalOrderId = orderId,
GlobalSign = OrderNumberHelper.IsValidEx(orderNumber)
? OrderNumberHelper.Normalize(orderNumber)
: null,
LocationId = locationId, LocationId = locationId,
Comment = reader["Comment"] == DBNull.Value ? null : reader["Comment"].ToString() Comment = reader["Comment"] == DBNull.Value ? null : reader["Comment"].ToString()
}; };
@ -224,65 +230,6 @@ WHERE Orders.OrderState = 'НОВЫЙ'
return orders.Values.ToList(); return orders.Values.ToList();
} }
private static string ResolveLocationIdCached(string consigneeName, Dictionary<string, string> cache)
{
var key = consigneeName ?? string.Empty;
if (cache.TryGetValue(key, out var cached))
{
return cached;
}
var locationId = ConsigneeHelper.ResolveLocationId(consigneeName);
cache[key] = locationId ?? string.Empty;
return cache[key];
}
private bool EnsureOrdersHaveLocationId(List<BuyerOrderRequest> orders)
{
var missing = orders
.Where(o => string.IsNullOrWhiteSpace(o.LocationId))
.Select(o => o.LocalOrderId)
.Distinct()
.ToList();
if (missing.Count == 0)
{
return true;
}
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
if (!HF_LocationId.PromptAndSave(Form.ActiveForm)
|| string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
AppDebugLog.Error("DataSender", "Отправка заказов: location_id не указан");
ToastNotification.ShowCustom(
"Не указан Location ID у аптеки заказа. Задайте его в «Грузополучатели» или в настройках.",
Color.DarkOrange,
Color.White);
return false;
}
ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId);
}
foreach (var order in orders.Where(o => string.IsNullOrWhiteSpace(o.LocationId)))
{
order.LocationId = AppConfig.LocationId;
}
if (orders.Any(o => string.IsNullOrWhiteSpace(o.LocationId)))
{
ToastNotification.ShowCustom(
"У части заказов нет Location ID. Укажите ID у грузополучателя.",
Color.DarkOrange,
Color.White);
return false;
}
return true;
}
private async Task<bool> SendOrderAsync(BuyerOrderRequest order) private async Task<bool> SendOrderAsync(BuyerOrderRequest order)
{ {
var options = new JsonSerializerOptions var options = new JsonSerializerOptions
@ -325,31 +272,10 @@ WHERE Orders.OrderState = 'НОВЫЙ'
return false; return false;
} }
var createdOrder = JsonSerializer.Deserialize<BuyerOrderCreateResult>(responseString, options); MarkOrderSent(order.LocalOrderId);
if (createdOrder == null || string.IsNullOrWhiteSpace(createdOrder.OrderID)) AppDebugLog.Info("DataSender", $"Заказ {order.LocalOrderId} отправлен");
{
throw new InvalidOperationException("Сервер не вернул order_id созданного заказа.");
}
// Старые серверы создавали Draft — дожимаем до Placed.
var remoteStatus = createdOrder.Status;
if (!string.Equals(remoteStatus, "Placed", StringComparison.OrdinalIgnoreCase))
{
remoteStatus = await PlaceOrderAsync(client, createdOrder.OrderID) ?? remoteStatus;
}
MarkOrderSent(order.LocalOrderId, createdOrder.OrderID, remoteStatus, createdOrder.GlobalSign);
AppDebugLog.Info("DataSender",
$"Заказ {order.LocalOrderId} отправлен, remote={createdOrder.OrderID}, status={remoteStatus}, sign={createdOrder.GlobalSign}");
return true; return true;
} }
catch (InvalidOperationException ex)
{
stopwatch.Stop();
AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}", ex);
ToastNotification.ShowError(ex.Message);
return false;
}
catch (Exception ex) catch (Exception ex)
{ {
stopwatch.Stop(); stopwatch.Stop();
@ -360,67 +286,7 @@ WHERE Orders.OrderState = 'НОВЫЙ'
} }
} }
private async Task<string> PlaceOrderAsync(HttpClient client, string remoteOrderId) private static void MarkOrderSent(string localOrderId)
{
var placePath = $"{BuyerOrdersEndpoint}/{Uri.EscapeDataString(remoteOrderId)}/place";
var fullUrl = new Uri(new Uri(_baseUrl.TrimEnd('/') + "/"), placePath.TrimStart('/')).ToString();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
AppDebugLog.ApiRequest("POST", fullUrl, string.Empty, $"place={remoteOrderId}");
try
{
using (var content = new StringContent(string.Empty, Encoding.UTF8, "application/json"))
using (HttpResponseMessage response = await client.PostAsync(placePath, content))
{
string responseString = await response.Content.ReadAsStringAsync();
stopwatch.Stop();
AppDebugLog.ApiResponse("POST", fullUrl, (int)response.StatusCode, stopwatch.ElapsedMilliseconds, responseString);
if (!response.IsSuccessStatusCode)
{
AppDebugLog.ApiHttpError(
"POST",
fullUrl,
(int)response.StatusCode,
responseString,
$"place={remoteOrderId}");
throw new InvalidOperationException(
$"Не удалось оформить заказ на сервере (HTTP {(int)response.StatusCode}). " +
"Заказ остался черновиком (Draft).");
}
try
{
using (var doc = JsonDocument.Parse(responseString))
{
if (doc.RootElement.TryGetProperty("status", out var statusEl))
{
return statusEl.GetString();
}
}
}
catch
{
// ignore parse — place succeeded
}
return "Placed";
}
}
catch (InvalidOperationException)
{
throw;
}
catch (Exception ex)
{
stopwatch.Stop();
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
throw new InvalidOperationException(
$"Не удалось оформить заказ на сервере: {ex.Message}", ex);
}
}
private static void MarkOrderSent(string localOrderId, string remoteOrderId, string remoteStatus, string globalSign)
{ {
if (string.IsNullOrWhiteSpace(localOrderId)) if (string.IsNullOrWhiteSpace(localOrderId))
{ {
@ -431,20 +297,9 @@ WHERE Orders.OrderState = 'НОВЫЙ'
{ {
con.Open(); con.Open();
using (var cmd = new SQLiteCommand( using (var cmd = new SQLiteCommand(
@"UPDATE [Orders] "UPDATE [Orders] SET [OrderState] = 'ОТПРАВЛЕН' WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'",
SET [OrderState] = @state,
[RemoteOrderId] = @remoteOrderId,
[OrderNumber] = CASE
WHEN @globalSign IS NOT NULL AND length(trim(@globalSign)) > 0 THEN @globalSign
ELSE [OrderNumber]
END
WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'",
con)) con))
{ {
cmd.Parameters.AddWithValue("@state", "ОТПРАВЛЕН");
cmd.Parameters.AddWithValue("@remoteOrderId", remoteOrderId ?? string.Empty);
cmd.Parameters.AddWithValue("@globalSign",
string.IsNullOrWhiteSpace(globalSign) ? (object)DBNull.Value : globalSign.Trim());
cmd.Parameters.AddWithValue("@id", localOrderId); cmd.Parameters.AddWithValue("@id", localOrderId);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
@ -481,9 +336,6 @@ WHERE Orders.OrderState = 'НОВЫЙ'
[JsonPropertyName("location_id")] [JsonPropertyName("location_id")]
public string LocationId { get; set; } public string LocationId { get; set; }
[JsonPropertyName("global_sign")]
public string GlobalSign { get; set; }
[JsonPropertyName("comment")] [JsonPropertyName("comment")]
public string Comment { get; set; } public string Comment { get; set; }
@ -505,16 +357,4 @@ WHERE Orders.OrderState = 'НОВЫЙ'
[JsonPropertyName("item_code")] [JsonPropertyName("item_code")]
public string ItemCode { get; set; } public string ItemCode { get; set; }
} }
public sealed class BuyerOrderCreateResult
{
[JsonPropertyName("order_id")]
public string OrderID { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("global_sign")]
public string GlobalSign { get; set; }
}
} }

View File

@ -1,321 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Электроннаяармация.Properties;
namespace Электроннаяармация.Classes
{
/// <summary>
/// Собирает доступные скидки покупателя/прайса/региона и вешает на позицию то, что пришло.
/// Отсутствующий источник = 0.
/// </summary>
public static class DiscountResolver
{
public static async Task<DiscountLookup> LoadAsync(ApiClient client, string loginHint)
{
var lookup = new DiscountLookup();
if (client == null)
{
return lookup;
}
var regionId = (Settings.Default.RegionId ?? string.Empty).Trim();
lookup.RegionId = regionId;
string buyerId = null;
try
{
var buyers = await client.GetBuyersAsync();
buyerId = ResolveBuyerId(buyers, loginHint);
lookup.BuyerId = buyerId;
AppDebugLog.Info(
"Discount",
string.IsNullOrWhiteSpace(buyerId)
? "buyer_id не найден — клиентская скидка будет 0"
: $"buyer_id={buyerId}");
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Не удалось загрузить покупателей: {ex.Message}");
}
List<BuyerPriceListAssignmentDto> assignments = null;
if (!string.IsNullOrWhiteSpace(buyerId))
{
try
{
assignments = await client.GetBuyerPriceListsAsync(buyerId);
AppDebugLog.Info("Discount", $"Назначений прайсов покупателя: {assignments?.Count ?? 0}");
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Не удалось загрузить buyer price-lists: {ex.Message}");
}
}
List<PriceListDto> priceLists = null;
try
{
priceLists = await client.GetPriceListsAsync();
AppDebugLog.Info("Discount", $"Прайс-листов: {priceLists?.Count ?? 0}");
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Не удалось загрузить price-lists: {ex.Message}");
}
EnrichAssignmentsFromPriceLists(assignments, priceLists);
if (assignments != null)
{
foreach (var a in assignments)
{
if (string.IsNullOrWhiteSpace(a?.PriceListId))
{
continue;
}
var plId = a.PriceListId.Trim();
lookup.ByPriceListId[plId] = new ItemDiscountParts
{
PriceListId = plId,
ClientDiscountPct = PositiveOrZero(a.MarkupPct),
PriceListDiscountPct = PositiveOrZero(a.DefaultMarkupPct),
RegionDiscountPct = 0m
};
IndexSupplierKeys(lookup, a.SupplierId, a.SupplierName, plId);
}
}
if (priceLists != null)
{
foreach (var pl in priceLists)
{
var plId = pl?.ResolvedId;
if (string.IsNullOrWhiteSpace(plId))
{
continue;
}
if (!lookup.ByPriceListId.TryGetValue(plId, out var parts))
{
parts = new ItemDiscountParts { PriceListId = plId };
lookup.ByPriceListId[plId] = parts;
}
if (parts.PriceListDiscountPct <= 0m && pl.DefaultMarkupPct.HasValue && pl.DefaultMarkupPct.Value > 0m)
{
parts.PriceListDiscountPct = pl.DefaultMarkupPct.Value;
}
IndexSupplierKeys(lookup, pl.SupplierId, pl.SupplierName, plId);
}
}
foreach (var plId in lookup.ByPriceListId.Keys.ToList())
{
try
{
var regions = await client.GetPriceListRegionsAsync(plId);
var regionPct = ResolveRegionDiscount(regions, regionId);
lookup.ByPriceListId[plId].RegionDiscountPct = regionPct;
}
catch (Exception ex)
{
AppDebugLog.Warning("Discount", $"Регионы для прайса {plId}: {ex.Message}");
}
}
AppDebugLog.Info(
"Discount",
$"Готово: прайсов={lookup.ByPriceListId.Count}, привязок по поставщику={lookup.BySupplierKey.Count}, region={regionId}");
return lookup;
}
public static ItemDiscountParts ResolveForItem(DiscountLookup lookup, PriceSummaryItem item)
{
if (lookup == null || item == null)
{
return new ItemDiscountParts();
}
var priceListId = (item.PriceListId ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(priceListId) &&
lookup.ByPriceListId.TryGetValue(priceListId, out var byId))
{
return Clone(byId);
}
var supplierId = (item.SupplierId ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(supplierId) &&
lookup.BySupplierKey.TryGetValue(NormalizeKey(supplierId), out var plFromSupplier) &&
lookup.ByPriceListId.TryGetValue(plFromSupplier, out var bySupplier))
{
return Clone(bySupplier);
}
var supplierName = (item.SupplierName ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(supplierName) &&
lookup.BySupplierKey.TryGetValue(NormalizeKey(supplierName), out var plFromName) &&
lookup.ByPriceListId.TryGetValue(plFromName, out var byName))
{
return Clone(byName);
}
return new ItemDiscountParts();
}
private static string ResolveBuyerId(List<BuyerDto> buyers, string loginHint)
{
if (buyers == null || buyers.Count == 0)
{
return null;
}
var login = (loginHint ?? string.Empty).Trim();
if (!string.IsNullOrWhiteSpace(login))
{
var matched = buyers.FirstOrDefault(b =>
string.Equals((b.Username ?? string.Empty).Trim(), login, StringComparison.OrdinalIgnoreCase) ||
string.Equals((b.Login ?? string.Empty).Trim(), login, StringComparison.OrdinalIgnoreCase) ||
string.Equals((b.Name ?? string.Empty).Trim(), login, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(matched?.ResolvedId))
{
return matched.ResolvedId;
}
}
if (buyers.Count == 1 && !string.IsNullOrWhiteSpace(buyers[0].ResolvedId))
{
return buyers[0].ResolvedId;
}
return buyers
.Select(b => b.ResolvedId)
.FirstOrDefault(id => !string.IsNullOrWhiteSpace(id));
}
private static void EnrichAssignmentsFromPriceLists(
List<BuyerPriceListAssignmentDto> assignments,
List<PriceListDto> priceLists)
{
if (assignments == null || priceLists == null || priceLists.Count == 0)
{
return;
}
var byId = priceLists
.Where(p => !string.IsNullOrWhiteSpace(p?.ResolvedId))
.GroupBy(p => p.ResolvedId, StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
foreach (var a in assignments)
{
if (a == null || string.IsNullOrWhiteSpace(a.PriceListId))
{
continue;
}
if (!byId.TryGetValue(a.PriceListId.Trim(), out var pl))
{
continue;
}
if (string.IsNullOrWhiteSpace(a.SupplierId))
{
a.SupplierId = pl.SupplierId;
}
if (string.IsNullOrWhiteSpace(a.SupplierName))
{
a.SupplierName = pl.SupplierName;
}
if ((!a.DefaultMarkupPct.HasValue || a.DefaultMarkupPct.Value <= 0m) &&
pl.DefaultMarkupPct.HasValue)
{
a.DefaultMarkupPct = pl.DefaultMarkupPct;
}
}
}
private static decimal ResolveRegionDiscount(List<PriceListRegionDto> regions, string regionId)
{
if (regions == null || regions.Count == 0)
{
return 0m;
}
PriceListRegionDto match = null;
if (!string.IsNullOrWhiteSpace(regionId))
{
match = regions.FirstOrDefault(r =>
string.Equals((r.RegionId ?? string.Empty).Trim(), regionId, StringComparison.OrdinalIgnoreCase) &&
(r.InWork == null || r.InWork.Value));
}
if (match == null)
{
match = regions.FirstOrDefault(r => r.InWork == true) ?? regions.FirstOrDefault();
}
return PositiveOrZero(match?.MarkupPct);
}
private static void IndexSupplierKeys(DiscountLookup lookup, string supplierId, string supplierName, string priceListId)
{
if (lookup == null || string.IsNullOrWhiteSpace(priceListId))
{
return;
}
if (!string.IsNullOrWhiteSpace(supplierId))
{
lookup.BySupplierKey[NormalizeKey(supplierId)] = priceListId;
}
if (!string.IsNullOrWhiteSpace(supplierName))
{
lookup.BySupplierKey[NormalizeKey(supplierName)] = priceListId;
}
}
private static ItemDiscountParts Clone(ItemDiscountParts source)
{
if (source == null)
{
return new ItemDiscountParts();
}
return new ItemDiscountParts
{
PriceListId = source.PriceListId,
PriceListDiscountPct = source.PriceListDiscountPct,
RegionDiscountPct = source.RegionDiscountPct,
ClientDiscountPct = source.ClientDiscountPct
};
}
private static decimal PositiveOrZero(decimal? value)
{
return value.HasValue && value.Value > 0m ? value.Value : 0m;
}
private static string NormalizeKey(string value)
{
return (value ?? string.Empty).Trim().ToLowerInvariant();
}
}
public sealed class DiscountLookup
{
public string BuyerId { get; set; }
public string RegionId { get; set; }
public Dictionary<string, ItemDiscountParts> ByPriceListId { get; } =
new Dictionary<string, ItemDiscountParts>(StringComparer.OrdinalIgnoreCase);
public Dictionary<string, string> BySupplierKey { get; } =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}

View File

@ -1,8 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SQLite; using System.Data.SQLite;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Электроннаяармация.Properties; using Электроннаяармация.Properties;
@ -28,29 +26,7 @@ namespace Электронная_Фармация.Classes
var client = new ApiClient(); var client = new ApiClient();
client.SetToken(token); client.SetToken(token);
AppDebugLog.Info("InvoiceSync", $"Старт синхронизации накладных. Сервер={client.BaseUrl}"); AppDebugLog.Info("InvoiceSync", $"Старт синхронизации накладных. Сервер={client.BaseUrl}");
var orders = await client.GetBuyerOrdersAsync(limit: 200); var items = await client.GetInvoicesAsync();
var items = new List<InvoiceApiItem>();
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
foreach (var order in orders)
{
if (string.Equals(order.Status, "Cancelled", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var detail = await client.GetBuyerOrderByIdAsync(order.OrderID);
if (detail == null)
{
continue;
}
detail.LocalOrderNumber = TryGetLocalOrderNumber(con, detail.OrderID);
items.Add(InvoiceApiItem.FromBuyerOrder(detail));
}
}
var saved = SaveInvoices(items); var saved = SaveInvoices(items);
AppDebugLog.Info("InvoiceSync", $"Накладные сохранены локально: {saved} шт."); AppDebugLog.Info("InvoiceSync", $"Накладные сохранены локально: {saved} шт.");
return saved; return saved;
@ -64,32 +40,30 @@ namespace Электронная_Фармация.Classes
private static int SaveInvoices(IReadOnlyList<InvoiceApiItem> items) private static int SaveInvoices(IReadOnlyList<InvoiceApiItem> items)
{ {
if (items.Count == 0)
{
return 0;
}
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString)) using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{ {
con.Open(); con.Open();
using (var tx = con.BeginTransaction()) using (var tx = con.BeginTransaction())
{ {
using (var cleanupItems = new SQLiteCommand(@"
DELETE FROM InvoiceItem
WHERE idInvoice IN (
SELECT idInvoice FROM Invoice WHERE ifnull(IsRequestedInvoice, 0) = 0
);", con, tx))
{
cleanupItems.ExecuteNonQuery();
}
using (var cleanupInvoices = new SQLiteCommand(
"DELETE FROM Invoice WHERE ifnull(IsRequestedInvoice, 0) = 0;",
con,
tx))
{
cleanupInvoices.ExecuteNonQuery();
}
foreach (var item in items) foreach (var item in items)
{ {
var invoiceId = UpsertInvoice(con, tx, item); using (var cmd = new SQLiteCommand(@"
SaveInvoiceItems(con, tx, invoiceId, item); INSERT OR REPLACE INTO Invoice (InvoiceNumber, invoiceDate, SupplierName, ConsigneesName, InvoiceSum, RefuseSum)
VALUES (@number, @date, @supplier, @consignee, @sum, @refuseSum)", con, tx))
{
cmd.Parameters.AddWithValue("@number", item.InvoiceNumber ?? string.Empty);
cmd.Parameters.AddWithValue("@date", item.InvoiceDate ?? string.Empty);
cmd.Parameters.AddWithValue("@supplier", item.SupplierName ?? string.Empty);
cmd.Parameters.AddWithValue("@consignee", item.ConsigneeName ?? string.Empty);
cmd.Parameters.AddWithValue("@sum", item.InvoiceSum ?? string.Empty);
cmd.Parameters.AddWithValue("@refuseSum", item.RefuseSum ?? string.Empty);
cmd.ExecuteNonQuery();
}
} }
tx.Commit(); tx.Commit();
@ -98,192 +72,5 @@ WHERE idInvoice IN (
return items.Count; return items.Count;
} }
private static int UpsertInvoice(SQLiteConnection con, SQLiteTransaction tx, InvoiceApiItem item)
{
using (var findCmd = new SQLiteCommand(@"
SELECT [idInvoice]
FROM [Invoice]
WHERE cast([SourceOrderId] as text) = @sourceOrderId
LIMIT 1;", con, tx))
{
findCmd.Parameters.AddWithValue("@sourceOrderId", item.SourceOrderId ?? string.Empty);
var existing = findCmd.ExecuteScalar();
if (existing != null && existing != DBNull.Value)
{
var invoiceId = Convert.ToInt32(existing, CultureInfo.InvariantCulture);
using (var updateCmd = new SQLiteCommand(@"
UPDATE [Invoice]
SET [InvoiceNumber] = @number,
[InvoiceDate] = @date,
[SupplierName] = @supplier,
[ConsigneesName] = @consignee,
[InvoiceSum] = @sum,
[RefuseSum] = @refuseSum,
[SourceOrderNumber] = @sourceOrderNumber,
[IsRequestedInvoice] = 0
WHERE [idInvoice] = @invoiceId;", con, tx))
{
updateCmd.Parameters.AddWithValue("@number", item.InvoiceNumber ?? string.Empty);
updateCmd.Parameters.AddWithValue("@date", item.InvoiceDate ?? string.Empty);
updateCmd.Parameters.AddWithValue("@supplier", item.SupplierName ?? string.Empty);
updateCmd.Parameters.AddWithValue("@consignee", item.ConsigneeName ?? string.Empty);
updateCmd.Parameters.AddWithValue("@sum", item.InvoiceSum ?? "0");
updateCmd.Parameters.AddWithValue("@refuseSum", item.RefuseSum ?? "0");
updateCmd.Parameters.AddWithValue("@sourceOrderNumber", item.InvoiceNumber ?? string.Empty);
updateCmd.Parameters.AddWithValue("@invoiceId", invoiceId);
updateCmd.ExecuteNonQuery();
}
using (var deleteItems = new SQLiteCommand("DELETE FROM [InvoiceItem] WHERE [idInvoice] = @invoiceId;", con, tx))
{
deleteItems.Parameters.AddWithValue("@invoiceId", invoiceId);
deleteItems.ExecuteNonQuery();
}
return invoiceId;
}
}
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,
@sum,
@refuseSum,
@sourceOrderId,
@sourceOrderNumber,
0
);
SELECT last_insert_rowid();", con, tx))
{
insertCmd.Parameters.AddWithValue("@number", item.InvoiceNumber ?? string.Empty);
insertCmd.Parameters.AddWithValue("@date", item.InvoiceDate ?? string.Empty);
insertCmd.Parameters.AddWithValue("@sum", item.InvoiceSum ?? "0");
insertCmd.Parameters.AddWithValue("@supplier", item.SupplierName ?? string.Empty);
insertCmd.Parameters.AddWithValue("@consignee", item.ConsigneeName ?? string.Empty);
insertCmd.Parameters.AddWithValue("@refuseSum", item.RefuseSum ?? "0");
insertCmd.Parameters.AddWithValue("@sourceOrderId", item.SourceOrderId ?? string.Empty);
insertCmd.Parameters.AddWithValue("@sourceOrderNumber", item.InvoiceNumber ?? string.Empty);
return Convert.ToInt32(insertCmd.ExecuteScalar(), CultureInfo.InvariantCulture);
}
}
private static void SaveInvoiceItems(SQLiteConnection con, SQLiteTransaction tx, int invoiceId, InvoiceApiItem item)
{
foreach (var invoiceItem in item.Items ?? Enumerable.Empty<InvoiceApiOrderItem>())
{
using (var cmd = 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,
'',
'',
'',
0,
''
)", con, tx))
{
cmd.Parameters.AddWithValue("@goodCode", ParseGoodCode(invoiceItem.GoodCode));
cmd.Parameters.AddWithValue("@good", invoiceItem.GoodName ?? string.Empty);
cmd.Parameters.AddWithValue("@invoiceId", invoiceId);
cmd.Parameters.AddWithValue("@qty", invoiceItem.Qty);
cmd.Parameters.AddWithValue("@price", invoiceItem.Price);
cmd.Parameters.AddWithValue("@sum", invoiceItem.Total);
cmd.ExecuteNonQuery();
}
}
}
private static int ParseGoodCode(string value)
{
int parsed;
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed) ? parsed : 0;
}
private static string TryGetLocalOrderNumber(SQLiteConnection con, string remoteOrderId)
{
if (string.IsNullOrWhiteSpace(remoteOrderId))
{
return null;
}
using (var cmd = new SQLiteCommand(@"
SELECT [OrderNumber]
FROM [Orders]
WHERE [RemoteOrderId] = @remoteOrderId
LIMIT 1;", con))
{
cmd.Parameters.AddWithValue("@remoteOrderId", remoteOrderId);
var value = cmd.ExecuteScalar();
return value == null || value == DBNull.Value ? null : value.ToString();
}
}
} }
} }

View File

@ -1,57 +1,29 @@
using System; using System;
namespace Электроннаяармация.Classes namespace Электроннаяармация.Classes
{ {
public static class MarkupHelper public static class MarkupHelper
{ {
public const decimal FallbackMarkupPercent = 0m; public const decimal FallbackMarkupPercent = 30m;
/// <summary>
/// Сумма доступных скидок. null / отрицательные значения считаются 0.
/// </summary>
public static decimal SumDiscountPercents(params decimal?[] parts)
{
decimal total = 0m;
if (parts == null)
{
return total;
}
foreach (var part in parts)
{
if (part.HasValue && part.Value > 0m)
{
total += part.Value;
}
}
return total;
}
public static decimal ResolveMarkupPercent(decimal? itemMarkup, decimal? clientMarkup, decimal fallbackMarkup) public static decimal ResolveMarkupPercent(decimal? itemMarkup, decimal? clientMarkup, decimal fallbackMarkup)
{ {
var total = SumDiscountPercents(itemMarkup, clientMarkup); if (itemMarkup.HasValue && itemMarkup.Value >= 0)
if (total > 0m)
{ {
return total; return itemMarkup.Value;
} }
return fallbackMarkup > 0m ? fallbackMarkup : FallbackMarkupPercent; if (clientMarkup.HasValue && clientMarkup.Value >= 0)
{
return clientMarkup.Value;
}
return fallbackMarkup > 0 ? fallbackMarkup : FallbackMarkupPercent;
} }
public static decimal ApplyMarkup(decimal basePrice, decimal markupPercent) public static decimal ApplyMarkup(decimal basePrice, decimal markupPercent)
{ {
return ApplyDiscount(basePrice, markupPercent); return basePrice + (basePrice * markupPercent / 100m);
}
/// <summary>
/// итоговая = base * (1 - discount/100), минимум 0. Без округления.
/// </summary>
public static decimal ApplyDiscount(decimal basePrice, decimal discountPercent)
{
var effectiveDiscount = Math.Max(0m, discountPercent);
var result = basePrice * (1m - effectiveDiscount / 100m);
return result < 0m ? 0m : result;
} }
public static bool TryParseMarkup(object value, out decimal markupPercent) public static bool TryParseMarkup(object value, out decimal markupPercent)
@ -87,12 +59,7 @@ namespace Электронная_Фармация.Classes
} }
var text = value.ToString()?.Trim().Replace(',', '.'); var text = value.ToString()?.Trim().Replace(',', '.');
return !string.IsNullOrWhiteSpace(text) && return !string.IsNullOrWhiteSpace(text) && decimal.TryParse(text, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out markupPercent);
decimal.TryParse(
text,
System.Globalization.NumberStyles.Any,
System.Globalization.CultureInfo.InvariantCulture,
out markupPercent);
} }
} }
} }

View File

@ -1,157 +0,0 @@
using System;
using System.Data.SQLite;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Электроннаяармация.Properties;
namespace Электроннаяармация.Classes
{
/// <summary>
/// Глобальные номера заказов вида EX-0000001.
/// Источник истины — сервер (никогда не повторяются между аптеками).
/// Offline fallback — локальный счётчик EX- (потом сервер может заменить при отправке).
/// </summary>
public static class OrderNumberHelper
{
private static readonly Regex ExPattern = new Regex(@"^EX-(\d{1,7})$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
/// <summary>
/// Выделяет номер для нового локального заказа.
/// Сначала пробует сервер /api/buyer/global-sign/next, иначе локальный EX-#######.
/// </summary>
public static string AllocateForNewOrder()
{
try
{
var fromServer = AllocateFromServerAsync().GetAwaiter().GetResult();
if (!string.IsNullOrWhiteSpace(fromServer))
{
return fromServer;
}
}
catch (Exception ex)
{
AppDebugLog.Info("OrderNumber", $"Сервер недоступен для GlobalSign: {ex.Message}");
}
return AllocateLocalFallback();
}
public static async Task<string> AllocateFromServerAsync()
{
var token = Settings.Default.stringToken;
var baseUrl = AppConfig.ApiBaseUrl;
if (string.IsNullOrWhiteSpace(token) || string.IsNullOrWhiteSpace(baseUrl))
{
return null;
}
using (var client = new HttpClient { Timeout = TimeSpan.FromSeconds(8) })
{
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Trim());
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
using (var response = await client.PostAsync("api/buyer/global-sign/next", new StringContent(string.Empty)).ConfigureAwait(false))
{
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
AppDebugLog.Info("OrderNumber", $"global-sign/next HTTP {(int)response.StatusCode}: {body}");
return null;
}
using (var doc = JsonDocument.Parse(body))
{
if (doc.RootElement.TryGetProperty("global_sign", out var el))
{
var sign = el.GetString();
if (IsValidEx(sign))
{
return Normalize(sign);
}
}
}
}
}
return null;
}
public static string AllocateLocalFallback()
{
long max = 0;
try
{
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand(
"SELECT OrderNumber FROM Orders WHERE OrderNumber LIKE 'EX-%'", con))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var n = ParseExNumber(reader[0]?.ToString());
if (n > max)
{
max = n;
}
}
}
}
}
catch (Exception ex)
{
AppDebugLog.Info("OrderNumber", $"Локальный max EX-: {ex.Message}");
}
return FormatEx(max + 1);
}
public static bool IsValidEx(string value)
{
return ParseExNumber(value) > 0;
}
public static string Normalize(string value)
{
var n = ParseExNumber(value);
return n > 0 ? FormatEx(n) : value?.Trim();
}
public static string FormatEx(long n)
{
if (n < 1)
{
n = 1;
}
if (n > 9999999)
{
return "EX-" + n.ToString();
}
return "EX-" + n.ToString("D7");
}
public static long ParseExNumber(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return 0;
}
var m = ExPattern.Match(value.Trim());
if (!m.Success)
{
return 0;
}
return long.TryParse(m.Groups[1].Value, out var n) ? n : 0;
}
}
}

View File

@ -16,9 +16,6 @@ namespace Электронная_Фармация.Classes
[JsonPropertyName("supplier_price_id")] [JsonPropertyName("supplier_price_id")]
public string SupplierPriceID { get; set; } public string SupplierPriceID { get; set; }
[JsonPropertyName("price_list_id")]
public string PriceListId { get; set; }
[JsonPropertyName("guid_es")] [JsonPropertyName("guid_es")]
public string GuidEs { get; set; } public string GuidEs { get; set; }

View File

@ -1,153 +0,0 @@
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace Электроннаяармация.Classes
{
/// <summary>
/// Лёгкая подсветка SQL в RichTextBox (ключевые слова, строки, комментарии, числа, параметры).
/// </summary>
public static class SqlSyntaxHighlighter
{
private static readonly Regex TokenRegex = new Regex(
@"(--[^\r\n]*)|(/\*[\s\S]*?\*/)|('(?:''|[^'])*')|(""(?:""""|[^""])*"")|(\[[^\]]*\])|(@[A-Za-z_][A-Za-z0-9_]*)|(\{\{[A-Za-z_][A-Za-z0-9_]*\}\})|(\b\d+(?:\.\d+)?\b)|(\b[A-Za-z_][A-Za-z0-9_]*\b)",
RegexOptions.Compiled);
private static readonly System.Collections.Generic.HashSet<string> Keywords =
new System.Collections.Generic.HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"SELECT", "FROM", "WHERE", "AND", "OR", "NOT", "IN", "IS", "NULL", "AS",
"JOIN", "INNER", "LEFT", "RIGHT", "OUTER", "CROSS", "ON", "USING",
"GROUP", "BY", "ORDER", "HAVING", "LIMIT", "OFFSET", "DISTINCT", "ALL",
"INSERT", "INTO", "VALUES", "UPDATE", "SET", "DELETE", "REPLACE",
"CREATE", "ALTER", "DROP", "TABLE", "INDEX", "VIEW", "TRIGGER", "IF", "EXISTS",
"PRIMARY", "KEY", "FOREIGN", "REFERENCES", "UNIQUE", "DEFAULT", "CHECK",
"BEGIN", "COMMIT", "ROLLBACK", "TRANSACTION", "PRAGMA", "EXPLAIN", "WITH",
"CASE", "WHEN", "THEN", "ELSE", "END", "BETWEEN", "LIKE", "GLOB", "ESCAPE",
"UNION", "INTERSECT", "EXCEPT", "CAST", "COLLATE", "ASC", "DESC",
"COUNT", "SUM", "AVG", "MIN", "MAX", "IFNULL", "COALESCE", "NULLIF",
"LENGTH", "TRIM", "UPPER", "LOWER", "SUBSTR", "REPLACE", "ROUND", "ABS",
"DATETIME", "DATE", "TIME", "STRFTIME", "ADD", "COLUMN", "RENAME", "TO"
};
private const int WM_SETREDRAW = 0x000B;
[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
public static void Apply(RichTextBox box, bool darkTheme)
{
if (box == null || box.IsDisposed)
{
return;
}
string text = box.Text ?? string.Empty;
int selStart = box.SelectionStart;
int selLength = box.SelectionLength;
int firstVisibleChar = box.GetCharIndexFromPosition(new Point(1, 1));
Color defaultFg = darkTheme ? Color.FromArgb(220, 220, 220) : Color.FromArgb(30, 30, 30);
Color keyword = darkTheme ? Color.FromArgb(86, 156, 214) : Color.FromArgb(0, 0, 180);
Color str = darkTheme ? Color.FromArgb(206, 145, 120) : Color.FromArgb(163, 21, 21);
Color comment = darkTheme ? Color.FromArgb(106, 153, 85) : Color.FromArgb(0, 128, 0);
Color number = darkTheme ? Color.FromArgb(181, 206, 168) : Color.FromArgb(9, 134, 88);
Color param = darkTheme ? Color.FromArgb(197, 134, 192) : Color.FromArgb(128, 0, 128);
Color ident = darkTheme ? Color.FromArgb(156, 220, 254) : Color.FromArgb(0, 100, 120);
SuspendRedraw(box);
try
{
box.SelectAll();
box.SelectionColor = defaultFg;
box.SelectionFont = box.Font;
foreach (Match match in TokenRegex.Matches(text))
{
Color color = defaultFg;
bool bold = false;
if (match.Groups[1].Success || match.Groups[2].Success)
{
color = comment;
}
else if (match.Groups[3].Success || match.Groups[4].Success || match.Groups[5].Success)
{
color = str;
}
else if (match.Groups[6].Success || match.Groups[7].Success)
{
color = param;
bold = true;
}
else if (match.Groups[8].Success)
{
color = number;
}
else if (match.Groups[9].Success)
{
var word = match.Groups[9].Value;
if (Keywords.Contains(word))
{
color = keyword;
bold = true;
}
else
{
color = ident;
}
}
if (color == defaultFg && !bold)
{
continue;
}
box.Select(match.Index, match.Length);
box.SelectionColor = color;
if (bold)
{
box.SelectionFont = new Font(box.Font, FontStyle.Bold);
}
else
{
box.SelectionFont = box.Font;
}
}
}
finally
{
box.Select(selStart, selLength);
try
{
box.Select(firstVisibleChar, 0);
var pt = box.GetPositionFromCharIndex(firstVisibleChar);
if (pt.Y < 0)
{
// keep best-effort scroll
}
}
catch
{
// ignore scroll restore issues
}
box.Select(selStart, selLength);
ResumeRedraw(box);
box.Invalidate();
}
}
private static void SuspendRedraw(Control control)
{
SendMessage(control.Handle, WM_SETREDRAW, IntPtr.Zero, IntPtr.Zero);
}
private static void ResumeRedraw(Control control)
{
SendMessage(control.Handle, WM_SETREDRAW, new IntPtr(1), IntPtr.Zero);
}
}
}

View File

@ -125,36 +125,15 @@ namespace Электронная_Фармация.Classes
} }
else if (control is Panel || control is UserControl || control is TableLayoutPanel) else if (control is Panel || control is UserControl || control is TableLayoutPanel)
{ {
var tag = control.Tag as string; if (control.Tag as string == "keep-bg")
if (tag == "keep-bg")
{ {
// preserve custom background // preserve custom background
} }
else if (tag == "chrome-child") else if (control.Tag as string == "chrome-child")
{ {
control.BackColor = theme.BackgroundPrimary; control.BackColor = theme.BackgroundPrimary;
control.ForeColor = theme.TextPrimary; control.ForeColor = theme.TextPrimary;
} }
else if (tag == "toolbar")
{
control.BackColor = theme.Surface;
control.ForeColor = theme.TextPrimary;
}
else if (tag == "status-footer")
{
control.BackColor = theme.CartHeaderBackground;
control.ForeColor = theme.TextPrimary;
}
else if (tag == "surface-bar")
{
control.BackColor = theme.SectionHeaderBackground;
control.ForeColor = theme.TextPrimary;
}
else if (tag == "markup-bar")
{
control.BackColor = theme.AccentPrimarySoft;
control.ForeColor = theme.TextPrimary;
}
else else
{ {
control.BackColor = theme.BackgroundSecondary; control.BackColor = theme.BackgroundSecondary;

View File

@ -1,302 +0,0 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Электроннаяармация.Classes
{
public sealed class UpdateCheckResult
{
public bool CheckedOk { get; set; }
public bool UpdateRequired { get; set; }
public string LocalVersion { get; set; }
public string LatestVersion { get; set; }
public string ReleaseUrl { get; set; }
public string DownloadUrl { get; set; }
public string ErrorMessage { get; set; }
}
/// <summary>
/// Checks Gitea Releases API for a newer desktop build.
/// </summary>
public static class UpdateChecker
{
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
public static UpdateCheckResult CheckForUpdate()
{
return CheckForUpdate(null, null, null, null);
}
/// <summary>
/// Overload for tests / smoke scripts (null = use AppConfig).
/// </summary>
public static UpdateCheckResult CheckForUpdate(
string baseUrl,
string repo,
string token,
string localVersionOverride)
{
try
{
return CheckForUpdateAsync(baseUrl, repo, token, localVersionOverride)
.GetAwaiter().GetResult();
}
catch (Exception ex)
{
AppDebugLog.Info("UpdateChecker", $"Проверка обновлений пропущена: {ex.Message}");
return new UpdateCheckResult
{
CheckedOk = false,
UpdateRequired = false,
LocalVersion = localVersionOverride ?? GetLocalVersionString(),
ErrorMessage = ex.Message
};
}
}
public static async Task<UpdateCheckResult> CheckForUpdateAsync()
{
return await CheckForUpdateAsync(null, null, null, null).ConfigureAwait(false);
}
public static async Task<UpdateCheckResult> CheckForUpdateAsync(
string baseUrlOverride,
string repoOverride,
string tokenOverride,
string localVersionOverride)
{
var localVersion = string.IsNullOrWhiteSpace(localVersionOverride)
? GetLocalVersionString()
: NormalizeVersion(localVersionOverride);
var result = new UpdateCheckResult
{
LocalVersion = localVersion
};
var baseUrl = (baseUrlOverride ?? AppConfig.UpdateCheckBaseUrl).TrimEnd('/');
var repo = (repoOverride ?? AppConfig.UpdateRepo).Trim('/');
if (string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(repo))
{
result.CheckedOk = false;
result.ErrorMessage = "UpdateCheckBaseUrl/UpdateRepo не заданы";
return result;
}
var url = $"{baseUrl}/api/v1/repos/{repo}/releases/latest";
using (var client = new HttpClient { Timeout = TimeSpan.FromSeconds(8) })
{
var token = tokenOverride ?? AppConfig.UpdateApiToken;
if (!string.IsNullOrWhiteSpace(token))
{
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("token", token.Trim());
}
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
AppDebugLog.Info("UpdateChecker", $"GET {url}");
HttpResponseMessage response;
try
{
response = await client.GetAsync(url).ConfigureAwait(false);
}
catch (Exception ex)
{
// Offline pharmacies must continue working.
AppDebugLog.Info("UpdateChecker", $"Сеть недоступна, обновление не блокируем: {ex.Message}");
result.CheckedOk = false;
result.ErrorMessage = ex.Message;
return result;
}
var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
AppDebugLog.Info("UpdateChecker",
$"releases/latest HTTP {(int)response.StatusCode}: {Truncate(body, 200)}");
result.CheckedOk = false;
result.ErrorMessage = $"HTTP {(int)response.StatusCode}";
return result;
}
var release = JsonSerializer.Deserialize<GiteaRelease>(body, JsonOptions);
if (release == null || string.IsNullOrWhiteSpace(release.TagName))
{
result.CheckedOk = false;
result.ErrorMessage = "Пустой ответ releases/latest";
return result;
}
var latest = NormalizeVersion(release.TagName);
result.LatestVersion = latest;
result.ReleaseUrl = string.IsNullOrWhiteSpace(release.HtmlUrl)
? $"{baseUrl}/{repo}/releases/tag/{release.TagName}"
: release.HtmlUrl;
result.DownloadUrl = PickZipAssetUrl(release) ?? result.ReleaseUrl;
result.CheckedOk = true;
result.UpdateRequired = IsRemoteNewer(latest, localVersion);
AppDebugLog.Info("UpdateChecker",
$"Локально={localVersion}, latest={latest}, updateRequired={result.UpdateRequired}");
return result;
}
}
public static string GetLocalVersionString()
{
try
{
var asm = Assembly.GetExecutingAssembly();
var file = asm.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version;
if (!string.IsNullOrWhiteSpace(file))
{
return NormalizeVersion(file);
}
return NormalizeVersion(asm.GetName().Version?.ToString() ?? "0.0.0.0");
}
catch
{
return "0.0.0.0";
}
}
/// <summary>
/// Returns true when remoteTag is strictly newer than localVersion.
/// Accepts "v1.0.9", "1.0.9", "1.0.9.0".
/// </summary>
public static bool IsRemoteNewer(string remoteTag, string localVersion)
{
if (!TryParseVersion(remoteTag, out var remote) ||
!TryParseVersion(localVersion, out var local))
{
return false;
}
return remote > local;
}
public static string NormalizeVersion(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return "0.0.0.0";
}
value = value.Trim();
if (value.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
value = value.Substring(1);
}
// Drop pre-release suffix: 1.0.9-beta -> 1.0.9
var dash = value.IndexOf('-');
if (dash > 0)
{
value = value.Substring(0, dash);
}
if (TryParseVersion(value, out var v))
{
return v.ToString();
}
return value;
}
public static bool TryParseVersion(string value, out Version version)
{
version = null;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
value = value.Trim();
if (value.StartsWith("v", StringComparison.OrdinalIgnoreCase))
{
value = value.Substring(1);
}
var dash = value.IndexOf('-');
if (dash > 0)
{
value = value.Substring(0, dash);
}
var parts = value.Split('.');
if (parts.Length == 1)
{
value = value + ".0.0.0";
}
else if (parts.Length == 2)
{
value = value + ".0.0";
}
else if (parts.Length == 3)
{
value = value + ".0";
}
return Version.TryParse(value, out version);
}
private static string PickZipAssetUrl(GiteaRelease release)
{
if (release?.Assets == null || release.Assets.Length == 0)
{
return null;
}
var zip = release.Assets.FirstOrDefault(a =>
!string.IsNullOrWhiteSpace(a?.Name) &&
a.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase));
if (zip == null)
{
zip = release.Assets.FirstOrDefault(a => !string.IsNullOrWhiteSpace(a?.BrowserDownloadUrl));
}
return zip?.BrowserDownloadUrl;
}
private static string Truncate(string s, int max)
{
if (string.IsNullOrEmpty(s) || s.Length <= max)
{
return s ?? string.Empty;
}
return s.Substring(0, max) + "...";
}
private sealed class GiteaRelease
{
[JsonPropertyName("tag_name")]
public string TagName { get; set; }
[JsonPropertyName("html_url")]
public string HtmlUrl { get; set; }
[JsonPropertyName("assets")]
public GiteaAsset[] Assets { get; set; }
}
private sealed class GiteaAsset
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("browser_download_url")]
public string BrowserDownloadUrl { get; set; }
}
}
}

View File

@ -17,11 +17,9 @@
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources> <GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
<NuGetPackageImportStamp> <NuGetPackageImportStamp>
</NuGetPackageImportStamp> </NuGetPackageImportStamp>
<ApplicationIcon>Resources\elfisa.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<DebugSymbols>true</DebugSymbols> <DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType> <DebugType>full</DebugType>
<Optimize>false</Optimize> <Optimize>false</Optimize>
@ -32,7 +30,6 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget> <PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<DebugType>pdbonly</DebugType> <DebugType>pdbonly</DebugType>
<Optimize>true</Optimize> <Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath> <OutputPath>bin\Release\</OutputPath>
@ -99,8 +96,6 @@
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Deployment" /> <Reference Include="System.Deployment" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
@ -109,11 +104,7 @@
<Compile Include="Classes\ApiClient.cs" /> <Compile Include="Classes\ApiClient.cs" />
<Compile Include="Classes\AppConfig.cs" /> <Compile Include="Classes\AppConfig.cs" />
<Compile Include="Classes\AppDebugLog.cs" /> <Compile Include="Classes\AppDebugLog.cs" />
<Compile Include="Classes\AutoUpdater.cs" />
<Compile Include="Classes\ConsigneeHelper.cs" />
<Compile Include="Classes\SqlSyntaxHighlighter.cs" />
<Compile Include="Classes\DataSender.cs" /> <Compile Include="Classes\DataSender.cs" />
<Compile Include="Classes\OrderNumberHelper.cs" />
<Compile Include="Classes\ErrorResponse.cs" /> <Compile Include="Classes\ErrorResponse.cs" />
<Compile Include="Classes\InvoiceExportService.cs" /> <Compile Include="Classes\InvoiceExportService.cs" />
<Compile Include="Classes\InvoiceRequestService.cs" /> <Compile Include="Classes\InvoiceRequestService.cs" />
@ -121,8 +112,6 @@
<Compile Include="Classes\LoginRequest.cs" /> <Compile Include="Classes\LoginRequest.cs" />
<Compile Include="Classes\LoginResponse.cs" /> <Compile Include="Classes\LoginResponse.cs" />
<Compile Include="Classes\MarkupHelper.cs" /> <Compile Include="Classes\MarkupHelper.cs" />
<Compile Include="Classes\BuyerDiscountDtos.cs" />
<Compile Include="Classes\DiscountResolver.cs" />
<Compile Include="Classes\AppServices.cs" /> <Compile Include="Classes\AppServices.cs" />
<Compile Include="Classes\UiDialogs.cs" /> <Compile Include="Classes\UiDialogs.cs" />
<Compile Include="Classes\UiThemeHelper.cs" /> <Compile Include="Classes\UiThemeHelper.cs" />
@ -134,7 +123,6 @@
<Compile Include="Classes\SuppliersItem.cs" /> <Compile Include="Classes\SuppliersItem.cs" />
<Compile Include="Classes\SupplierResponse.cs" /> <Compile Include="Classes\SupplierResponse.cs" />
<Compile Include="Classes\ToastNotification.cs" /> <Compile Include="Classes\ToastNotification.cs" />
<Compile Include="Classes\UpdateChecker.cs" />
<Compile Include="Form1.cs"> <Compile Include="Form1.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@ -201,18 +189,6 @@
<Compile Include="HelpForms\HF_Registration.Designer.cs"> <Compile Include="HelpForms\HF_Registration.Designer.cs">
<DependentUpon>HF_Registration.cs</DependentUpon> <DependentUpon>HF_Registration.cs</DependentUpon>
</Compile> </Compile>
<Compile Include="HelpForms\HF_Settings.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="HelpForms\HF_Settings.Designer.cs">
<DependentUpon>HF_Settings.cs</DependentUpon>
</Compile>
<Compile Include="HelpForms\HF_UpdateAvailable.cs">
<SubType>Form</SubType>
</Compile>
<Compile Include="HelpForms\HF_UpdateAvailable.Designer.cs">
<DependentUpon>HF_UpdateAvailable.cs</DependentUpon>
</Compile>
<Compile Include="HelpForms\HF_UploadDataToServer.cs"> <Compile Include="HelpForms\HF_UploadDataToServer.cs">
<SubType>Form</SubType> <SubType>Form</SubType>
</Compile> </Compile>
@ -248,9 +224,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>
@ -328,6 +301,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Include="App.config" />
<Content Include="Data\efClient.db">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Resources\JVMarkOfGood.jpg" /> <None Include="Resources\JVMarkOfGood.jpg" />
@ -358,7 +334,4 @@
<Import Project="packages\EntityFramework.6.4.4\build\EntityFramework.targets" Condition="Exists('packages\EntityFramework.6.4.4\build\EntityFramework.targets')" /> <Import Project="packages\EntityFramework.6.4.4\build\EntityFramework.targets" Condition="Exists('packages\EntityFramework.6.4.4\build\EntityFramework.targets')" />
<Import Project="packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets" Condition="Exists('packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets')" /> <Import Project="packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets" Condition="Exists('packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets')" />
<Import Project="packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets" Condition="Exists('packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets')" /> <Import Project="packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets" Condition="Exists('packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets')" />
<ItemGroup>
<None Include="Resources\elfisa.ico" />
</ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,4 @@
namespace Электроннаяармация namespace Электроннаяармация
{ {
partial class ElectroPharmacy partial class ElectroPharmacy
{ {
@ -381,11 +381,8 @@ namespace Электронная_Фармация
this.ClientSize = new System.Drawing.Size(1089, 554); this.ClientSize = new System.Drawing.Size(1089, 554);
this.Controls.Add(this.bindControl); this.Controls.Add(this.bindControl);
this.Controls.Add(this.tabContent); this.Controls.Add(this.tabContent);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = true;
this.MinimizeBox = true;
this.Margin = new System.Windows.Forms.Padding(4); this.Margin = new System.Windows.Forms.Padding(4);
this.MinimumSize = new System.Drawing.Size(900, 620);
this.Name = "ElectroPharmacy"; this.Name = "ElectroPharmacy";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Электронная Фармация"; this.Text = "Электронная Фармация";

View File

@ -23,12 +23,10 @@ namespace Электронная_Фармация
private ToastService _toastService; private ToastService _toastService;
private LoadingOverlay _loadingOverlay; private LoadingOverlay _loadingOverlay;
private ModernAppHeader _appHeader; private ModernAppHeader _appHeader;
private ModernTitleBar _titleBar;
private ModernDocumentTabStrip _documentTabStrip; private ModernDocumentTabStrip _documentTabStrip;
private Panel _documentHost; private Panel _documentHost;
private Panel _statusFooter; private Panel _statusFooter;
private Label _lblCurrentUser; private Label _lblCurrentUser;
private ModernButton _btnSettings;
private ContextMenuStrip _directoryMenu; private ContextMenuStrip _directoryMenu;
private ContextMenuStrip _documentTabMenu; private ContextMenuStrip _documentTabMenu;
private ToolStripMenuItem _documentCloseTabMenuItem; private ToolStripMenuItem _documentCloseTabMenuItem;
@ -39,9 +37,6 @@ namespace Электронная_Фармация
Font = new Font("Segoe UI", 12f); Font = new Font("Segoe UI", 12f);
AutoScaleMode = AutoScaleMode.Dpi; AutoScaleMode = AutoScaleMode.Dpi;
KeyPreview = true; KeyPreview = true;
FormBorderStyle = FormBorderStyle.None;
MaximizeBox = true;
MinimizeBox = true;
MinimumSize = new Size(900, 620); MinimumSize = new Size(900, 620);
bindControl.Visible = false; bindControl.Visible = false;
@ -51,22 +46,17 @@ namespace Электронная_Фармация
AppServices.Initialize(_toastService); AppServices.Initialize(_toastService);
_loadingOverlay = new LoadingOverlay { Name = "loadingOverlay", Dock = DockStyle.Fill }; _loadingOverlay = new LoadingOverlay { Name = "loadingOverlay", Dock = DockStyle.Fill };
_titleBar = new ModernTitleBar { Name = "titleBar" };
_titleBar.Attach(this);
_titleBar.ApplyTheme(ThemeManager.Colors);
_appHeader = new ModernAppHeader { Name = "appHeader", Dock = DockStyle.Top }; _appHeader = new ModernAppHeader { Name = "appHeader", Dock = DockStyle.Top };
_appHeader.SetItems(new[] _appHeader.SetItems(new[]
{ {
new SidebarItem { Key = "price", Text = "Прайс-лист", IconGlyph = "\uE14C" }, new SidebarItem { Key = "price", Text = "ПРАЙС-ЛИСТ", IconGlyph = "\uE8FD" },
new SidebarItem { Key = "orders", Text = "Заказы", IconGlyph = "\uE7BF" }, new SidebarItem { Key = "orders", Text = "ЗАКАЗЫ", IconGlyph = "\uE719" },
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 = "\uE711" },
new SidebarItem { Key = "refusals", Text = "Отказы", IconGlyph = "\uE7BA" }, new SidebarItem { Key = "directory", Text = "СПРАВОЧНИК", IconGlyph = "\uE8FD" },
new SidebarItem { Key = "directory", Text = "Справочник", IconGlyph = "\uE8F1" }, new SidebarItem { Key = "auth", Text = "АВТОРИЗАЦИЯ", IconGlyph = "\uE192" }
new SidebarItem { Key = "auth", Text = "Вход", IconGlyph = "\uE192" }
}); });
_documentTabStrip = new ModernDocumentTabStrip { Name = "documentTabStrip", Dock = DockStyle.Top }; _documentTabStrip = new ModernDocumentTabStrip { Name = "documentTabStrip", Dock = DockStyle.Top };
@ -76,22 +66,16 @@ namespace Электронная_Фармация
{ {
Name = "statusFooter", Name = "statusFooter",
Dock = DockStyle.Bottom, Dock = DockStyle.Bottom,
Height = 32, Height = 30,
Tag = "status-footer", Tag = "status-footer",
Padding = new Padding(12, 0, 8, 0) Padding = new Padding(12, 0, 16, 0)
}; };
_statusFooter.Paint += (_, e) => _statusFooter.Paint += (_, e) =>
{ {
var theme = ThemeManager.Colors; var border = ThemeManager.Colors.Border;
var g = e.Graphics; using (var pen = new Pen(border))
using (var fill = new SolidBrush(theme.CartHeaderBackground))
{ {
g.FillRectangle(fill, _statusFooter.ClientRectangle); e.Graphics.DrawLine(pen, 0, 0, _statusFooter.Width, 0);
}
using (var pen = new Pen(theme.AccentPrimaryBorder))
{
g.DrawLine(pen, 0, 0, _statusFooter.Width, 0);
} }
}; };
@ -99,49 +83,23 @@ namespace Электронная_Фармация
{ {
Name = "lblCurrentUser", Name = "lblCurrentUser",
Dock = DockStyle.Fill, Dock = DockStyle.Fill,
TextAlign = ContentAlignment.MiddleLeft, TextAlign = ContentAlignment.MiddleRight,
AutoEllipsis = true, AutoEllipsis = true,
Tag = "muted", Tag = "muted"
Padding = new Padding(0, 0, 4, 0)
}; };
_btnSettings = new ModernButton
{
Name = "btnSettings",
Dock = DockStyle.Right,
Margin = new Padding(0, 2, 2, 2),
Text = "Настройки",
CompactIconMode = false,
AllowTextEllipsis = false,
MinimumSize = new Size(88, 22),
Size = new Size(88, 22),
Padding = new Padding(6, 0, 6, 0),
Font = new Font("Segoe UI Semibold", 8.5f, FontStyle.Bold),
TabIndex = 0
};
_btnSettings.Click += (_, __) => OpenSettingsDialog();
// Сначала Fill (пользователь), потом Right (кнопка) — кнопка справа, текст не перекрывается.
_statusFooter.Controls.Add(_lblCurrentUser); _statusFooter.Controls.Add(_lblCurrentUser);
_statusFooter.Controls.Add(_btnSettings);
// Клик по строке "аптека" открывает выбор грузополучателя без перезапуска приложения.
// Т.к. метка у нас одна (Dock=Fill), обработчик навешиваем на весь Label.
_lblCurrentUser.Cursor = Cursors.Hand;
_lblCurrentUser.Click += (_, __) => SwitchConsignee();
Controls.Add(_loadingOverlay); Controls.Add(_loadingOverlay);
Controls.Add(_documentHost); Controls.Add(_documentHost);
Controls.Add(_statusFooter); Controls.Add(_statusFooter);
Controls.Add(_documentTabStrip); Controls.Add(_documentTabStrip);
Controls.Add(_appHeader); Controls.Add(_appHeader);
Controls.Add(_titleBar);
_directoryMenu = new ContextMenuStrip(); _directoryMenu = new ContextMenuStrip();
var contractorsItem = new ToolStripMenuItem("Поставщики"); var contractorsItem = new ToolStripMenuItem("Поставщики");
contractorsItem.Click += (_, __) => tsItemContractors_Click(_, __); contractorsItem.Click += (_, __) => tsItemContractors_Click(_, __);
var consigneesItem = new ToolStripMenuItem("Грузополучатели"); var consigneesItem = new ToolStripMenuItem("Грузополучатели");
consigneesItem.Click += (_, __) => tsConsignee_Click(_, __); consigneesItem.Click += (_, __) => tsConsignee_Click(_, __);
var switchConsigneeItem = new ToolStripMenuItem("Сменить грузополучателя");
switchConsigneeItem.Click += (_, __) => SwitchConsignee();
var invoicesItem = new ToolStripMenuItem("Накладные"); var invoicesItem = new ToolStripMenuItem("Накладные");
invoicesItem.Click += (_, __) => tsItemInvoices_Click(_, __); invoicesItem.Click += (_, __) => tsItemInvoices_Click(_, __);
var debugItem = new ToolStripMenuItem("Отладка"); var debugItem = new ToolStripMenuItem("Отладка");
@ -150,7 +108,6 @@ namespace Электронная_Фармация
{ {
contractorsItem, contractorsItem,
consigneesItem, consigneesItem,
switchConsigneeItem,
invoicesItem, invoicesItem,
new ToolStripSeparator(), new ToolStripSeparator(),
debugItem debugItem
@ -183,24 +140,12 @@ namespace Электронная_Фармация
{ {
UiThemeHelper.ApplyToControlTree(this); UiThemeHelper.ApplyToControlTree(this);
UpdateThemeToggleState(); UpdateThemeToggleState();
_titleBar?.ApplyTheme(ThemeManager.Colors);
WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors); WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
}; };
HandleCreated += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors); HandleCreated += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
} }
protected override void WndProc(ref Message m)
{
if (FormBorderStyle == FormBorderStyle.None
&& WindowChromeHelper.TryHandleNcHitTest(this, ref m))
{
return;
}
base.WndProc(ref m);
}
private void UpdateThemeToggleState() private void UpdateThemeToggleState()
{ {
if (_documentTabStrip != null) if (_documentTabStrip != null)
@ -221,49 +166,14 @@ namespace Электронная_Фармация
{ {
ThemeManager.CurrentVariant = Settings.Default.UseDarkTheme ThemeManager.CurrentVariant = Settings.Default.UseDarkTheme
? ThemeVariant.DarkProductivity ? ThemeVariant.DarkProductivity
: ThemeVariant.ClinicalPro; : ThemeVariant.PharmaTrust;
_themeService.Initialize(this); _themeService.Initialize(this);
UpdateThemeToggleState(); UpdateThemeToggleState();
UiThemeHelper.ApplyToControlTree(this); UiThemeHelper.ApplyToControlTree(this);
_titleBar?.ApplyTheme(ThemeManager.Colors);
WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors); WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
try
{
Icon = CreateAppIcon();
}
catch
{
// keep default
}
RefreshCurrentUserStatus(); RefreshCurrentUserStatus();
} }
private static Icon CreateAppIcon()
{
using (var bmp = new Bitmap(32, 32))
using (var g = Graphics.FromImage(bmp))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.Clear(Color.Transparent);
using (var brush = new SolidBrush(Color.FromArgb(15, 118, 110)))
{
g.FillEllipse(brush, 1, 1, 30, 30);
}
using (var pen = new Pen(Color.White, 3f)
{
StartCap = System.Drawing.Drawing2D.LineCap.Round,
EndCap = System.Drawing.Drawing2D.LineCap.Round
})
{
g.DrawLine(pen, 16, 8, 16, 24);
g.DrawLine(pen, 8, 16, 24, 16);
}
return Icon.FromHandle(bmp.GetHicon());
}
}
private void RefreshCurrentUserStatus() private void RefreshCurrentUserStatus()
{ {
if (_lblCurrentUser == null) if (_lblCurrentUser == null)
@ -273,39 +183,18 @@ namespace Электронная_Фармация
var login = Settings.Default.stringLogin?.Trim(); var login = Settings.Default.stringLogin?.Trim();
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken); var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
var pharmacy = ConsigneeName?.Trim();
if (hasToken &&
(string.IsNullOrWhiteSpace(pharmacy) || pharmacy == "Прайс-Лист"))
{
pharmacy = ConsigneeHelper.GetDefaultConsigneeName();
if (!string.IsNullOrWhiteSpace(pharmacy) &&
(string.IsNullOrWhiteSpace(ConsigneeName) || ConsigneeName == "Прайс-Лист"))
{
ConsigneeName = pharmacy;
}
}
string userPart;
if (!string.IsNullOrWhiteSpace(login) && hasToken) if (!string.IsNullOrWhiteSpace(login) && hasToken)
{ {
userPart = $"Пользователь: {login}"; _lblCurrentUser.Text = $"Пользователь: {login}";
} }
else if (!string.IsNullOrWhiteSpace(login)) else if (!string.IsNullOrWhiteSpace(login))
{ {
userPart = $"Пользователь: {login} (не авторизован)"; _lblCurrentUser.Text = $"Пользователь: {login} (не авторизован)";
} }
else else
{ {
userPart = "Пользователь не авторизован"; _lblCurrentUser.Text = "Пользователь не авторизован";
}
if (hasToken && !string.IsNullOrWhiteSpace(pharmacy) && pharmacy != "Прайс-Лист")
{
_lblCurrentUser.Text = $"{userPart} | Аптека: {pharmacy}";
}
else
{
_lblCurrentUser.Text = $"{userPart} | Аптека: не выбрана";
} }
} }
@ -322,9 +211,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 +389,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);

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Data; using System.Data;
@ -14,7 +14,6 @@ using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Elfisa.UI.Theming; using Elfisa.UI.Theming;
using Электроннаяармация.Classes; using Электроннаяармация.Classes;
using Электроннаяармация.Properties;
using Электроннаяармация.Forms; using Электроннаяармация.Forms;
using Электроннаяармация.HelpForms; using Электроннаяармация.HelpForms;
using Электроннаяармация.UserControls; using Электроннаяармация.UserControls;
@ -67,102 +66,25 @@ namespace Электронная_Фармация
{ {
FinalizeModernShell(); FinalizeModernShell();
// Принудительное обновление: если на Gitea есть более новый Release — не пускаем в работу.
// При недоступной сети не блокируем (офлайн-аптека должна открываться).
try
{
var update = UpdateChecker.CheckForUpdate();
if (update != null && update.UpdateRequired)
{
using (var dlg = new HF_UpdateAvailable(update))
{
UiThemeHelper.ApplyToControlTree(dlg);
dlg.ShowDialog(this);
}
// Жёстко завершаем процесс, чтобы updater смог заменить exe.
Environment.Exit(0);
return;
}
}
catch (Exception ex)
{
AppDebugLog.Info("App", $"Проверка обновлений пропущена: {ex.Message}");
}
FCheckDatabaseIntegrary fCheckDatabaseIntegrary = new FCheckDatabaseIntegrary(); FCheckDatabaseIntegrary fCheckDatabaseIntegrary = new FCheckDatabaseIntegrary();
UiThemeHelper.ApplyToControlTree(fCheckDatabaseIntegrary); UiThemeHelper.ApplyToControlTree(fCheckDatabaseIntegrary);
fCheckDatabaseIntegrary.ShowDialog(); fCheckDatabaseIntegrary.ShowDialog();
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
// Окно выбора грузополучателя открываем только после авторизации.
// Если аптек одна — не открываем окно вообще.
int consigneeCount = 0;
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand("SELECT COUNT(*) FROM Consignees", con))
{
consigneeCount = Convert.ToInt32(cmd.ExecuteScalar());
}
}
if (hasToken && consigneeCount > 1)
{
var consigneeDialog = new HFConsignees { ParentForm = this }; var consigneeDialog = new HFConsignees { ParentForm = this };
UiThemeHelper.ApplyToControlTree(consigneeDialog); UiThemeHelper.ApplyToControlTree(consigneeDialog);
consigneeDialog.ShowDialog(); consigneeDialog.ShowDialog();
}
if (string.IsNullOrWhiteSpace(ConsigneeName))
{
var defaultName = ConsigneeHelper.GetDefaultConsigneeName();
if (!string.IsNullOrWhiteSpace(defaultName))
{
ConsigneeName = defaultName;
ConsigneeHelper.SetActiveConsignee(defaultName);
}
}
if (!HasDocumentTabs()) if (!HasDocumentTabs())
{ {
loadDefaultPriceList(); loadDefaultPriceList();
} }
RefreshCurrentUserStatus();
} }
void loadDefaultPriceList() void loadDefaultPriceList()
{ {
var consignee = !string.IsNullOrWhiteSpace(ConsigneeName) var ucPriceList = new UCPriceList("Прайс-Лист");
? ConsigneeName OpenDocumentTab("Прайс-лист", "price", ucPriceList, allowDuplicate: true);
: ConsigneeHelper.GetDefaultConsigneeName();
if (string.IsNullOrWhiteSpace(consignee))
{
consignee = "Прайс-Лист";
}
else
{
ConsigneeName = consignee;
}
var title = consignee == "Прайс-Лист"
? "Прайс-лист"
: $"Прайс-лист ({consignee})";
// На смене аптеки не должно появляться много вкладок прайса.
// Поэтому перед открытием закрываем все вкладки с navKey="price".
ConsigneeHelper.SyncCartToPriceList(consignee);
foreach (var tab in _documentTabStrip.Tabs.Where(t => t.NavKey == "price").ToList())
{
CloseDocumentTab(tab.Id);
}
var ucPriceList = new UCPriceList(consignee);
OpenDocumentTab(title, "price", ucPriceList, allowDuplicate: false);
ucPriceList.dgvPriceList.Focus(); ucPriceList.dgvPriceList.Focus();
RefreshCurrentUserStatus();
} }
#region загрузкаПрайсЛиста_старая версия #region загрузкаПрайсЛиста_старая версия
@ -217,21 +139,9 @@ namespace Электронная_Фармация
private void tsConsignee_Click(object sender, EventArgs e) private void tsConsignee_Click(object sender, EventArgs e)
{ {
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы выбрать грузополучателя.", "Авторизация", this);
return;
}
var fConsignees = new FConsignees(); var fConsignees = new FConsignees();
UiThemeHelper.ApplyToControlTree(fConsignees); UiThemeHelper.ApplyToControlTree(fConsignees);
var dr = fConsignees.ShowDialog(this); fConsignees.ShowDialog();
if (dr == DialogResult.OK &&
!string.IsNullOrWhiteSpace(fConsignees.SelectedConsigneeNameAfterSave))
{
showMePriceList(fConsignees.SelectedConsigneeNameAfterSave);
}
} }
private void tsItemInvoices_Click(object sender, EventArgs e) private void tsItemInvoices_Click(object sender, EventArgs e)
@ -254,38 +164,9 @@ namespace Электронная_Фармация
public void showMePriceList(string consignee) public void showMePriceList(string consignee)
{ {
ConsigneeName = consignee ?? string.Empty; ConsigneeName = consignee ?? string.Empty;
if (!string.IsNullOrWhiteSpace(ConsigneeName))
{
ConsigneeHelper.SetActiveConsignee(ConsigneeName);
}
// Перед открытием прайс-листа синхронизируем Zakaz/SummaZakaza с корзиной активной аптеки.
ConsigneeHelper.SyncCartToPriceList(ConsigneeName);
var ucPriceList = new UCPriceList(consignee); var ucPriceList = new UCPriceList(consignee);
foreach (var tab in _documentTabStrip.Tabs.Where(t => t.NavKey == "price").ToList()) OpenDocumentTab($"Прайс-лист ({consignee})", "price", ucPriceList, allowDuplicate: true);
{
CloseDocumentTab(tab.Id);
}
OpenDocumentTab($"Прайс-лист ({consignee})", "price", ucPriceList, allowDuplicate: false);
ucPriceList.dgvPriceList.Focus(); ucPriceList.dgvPriceList.Focus();
RefreshCurrentUserStatus();
}
private void SwitchConsignee()
{
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы сменить грузополучателя.", "Авторизация", this);
return;
}
var consigneeDialog = new HFConsignees { ParentForm = this };
UiThemeHelper.ApplyToControlTree(consigneeDialog);
consigneeDialog.ShowDialog(this);
RefreshCurrentUserStatus();
} }
private void tsDebug_Click(object sender, EventArgs e) private void tsDebug_Click(object sender, EventArgs e)
@ -303,9 +184,7 @@ namespace Электронная_Фармация
private void tsExchangeLoad_Click(object sender, EventArgs e) private void tsExchangeLoad_Click(object sender, EventArgs e)
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
var consigneeSafe = (string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName) string qShowMeCoountDataFromTempOrder = "select count(*) from TempOrderItems";
?.Replace("'", "''");
string qShowMeCoountDataFromTempOrder = $"select count(*) from TempOrderItems where ConsigneeName = '{consigneeSafe}'";
int countDataTempOrder = 0; int countDataTempOrder = 0;
using (SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB)) using (SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
@ -329,8 +208,6 @@ namespace Электронная_Фармация
ClearDocumentTabs(); ClearDocumentTabs();
HF_DownloadDataFromServer hF_DownloadDataFromServer = new HF_DownloadDataFromServer(); HF_DownloadDataFromServer hF_DownloadDataFromServer = new HF_DownloadDataFromServer();
hF_DownloadDataFromServer.ActiveConsigneeName =
string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName;
UiThemeHelper.ApplyToControlTree(hF_DownloadDataFromServer); UiThemeHelper.ApplyToControlTree(hF_DownloadDataFromServer);
DialogResult downloadResult = DialogResult.Cancel; DialogResult downloadResult = DialogResult.Cancel;
if (countDataTempOrder > 0) if (countDataTempOrder > 0)
@ -707,85 +584,6 @@ namespace Электронная_Фармация
var hF_Registration = new HF_Registration(); var hF_Registration = new HF_Registration();
UiThemeHelper.ApplyToControlTree(hF_Registration); UiThemeHelper.ApplyToControlTree(hF_Registration);
if (hF_Registration.ShowDialog() == DialogResult.OK) if (hF_Registration.ShowDialog() == DialogResult.OK)
{
// После успешной авторизации сначала выбираем грузополучателя,
// и только потом (если нужно) запрашиваем Location ID.
int consigneeCount;
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand("SELECT COUNT(*) FROM Consignees", con))
{
consigneeCount = Convert.ToInt32(cmd.ExecuteScalar());
}
}
if (consigneeCount <= 1)
{
var defaultName = ConsigneeHelper.GetDefaultConsigneeName();
if (!string.IsNullOrWhiteSpace(defaultName))
{
ConsigneeName = defaultName;
ConsigneeHelper.SetActiveConsignee(defaultName);
}
}
else
{
var consigneeDialog = new HFConsignees { ParentForm = this };
UiThemeHelper.ApplyToControlTree(consigneeDialog);
if (consigneeDialog.ShowDialog(this) != DialogResult.OK)
{
// Без выбора грузополучателя дальше не идём.
return;
}
}
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
var consigneeName = ConsigneeName ?? string.Empty;
var consigneeAddress = string.Empty;
if (!string.IsNullOrWhiteSpace(consigneeName))
{
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
con.Open();
using (var cmd = new SQLiteCommand(
@"SELECT ifnull(ConsigneesAddress, '')
FROM Consignees
WHERE ConsigneesName = @name
LIMIT 1",
con))
{
cmd.Parameters.AddWithValue("@name", consigneeName.Trim());
consigneeAddress = cmd.ExecuteScalar()?.ToString() ?? string.Empty;
}
}
}
if (!HF_LocationId.PromptAndSave(this, consigneeName, consigneeAddress))
{
ToastNotification.ShowCustom(
"Location ID не указан. Укажите его перед отправкой заказов.",
Color.DarkOrange,
Color.White);
return;
}
}
if (!string.IsNullOrWhiteSpace(AppConfig.LocationId))
{
ConsigneeHelper.ApplyLocationIdToEmptyConsignees(AppConfig.LocationId);
}
RefreshCurrentUserStatus();
}
}
private void OpenSettingsDialog()
{
var settingsForm = new HF_Settings();
UiThemeHelper.ApplyToControlTree(settingsForm);
if (settingsForm.ShowDialog(this) == DialogResult.OK)
{ {
RefreshCurrentUserStatus(); RefreshCurrentUserStatus();
} }
@ -793,38 +591,10 @@ namespace Электронная_Фармация
private async void tsBtnSentData_Click(object sender, EventArgs e) private async void tsBtnSentData_Click(object sender, EventArgs e)
{ {
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы отправить заказы.", "Авторизация", this);
return;
}
DialogResult drUpdateData = UiDialogs.ConfirmYesNoCancel("Будут отправлены все сохранённые заказы. Вы уверены?", "Отправка заказов", this); DialogResult drUpdateData = UiDialogs.ConfirmYesNoCancel("Будут отправлены все сохранённые заказы. Вы уверены?", "Отправка заказов", this);
if (drUpdateData == DialogResult.Yes) if (drUpdateData == DialogResult.Yes)
{ {
var nowUtc = DateTime.UtcNow;
var lastPriceUpdateUtc = Settings.Default.LastPriceUpdateUtc;
var lastPriceReminderUtc = Settings.Default.LastPriceReminderUtc;
var priceIsStale = lastPriceUpdateUtc == DateTime.MinValue ||
(nowUtc - lastPriceUpdateUtc) > TimeSpan.FromHours(24);
var canRemind = lastPriceReminderUtc == DateTime.MinValue ||
(nowUtc - lastPriceReminderUtc) > TimeSpan.FromHours(24);
if (priceIsStale && canRemind)
{
// Напоминание не блокирует отправку.
UiDialogs.ShowInfo(
"Прайс-лист не обновлялся более 24 часов. Рекомендуем обновить прайс перед отправкой заказов.",
"Прайс-лист",
this);
Settings.Default.LastPriceReminderUtc = nowUtc;
Settings.Default.Save();
}
ShowShellLoading("Отправка заказов..."); ShowShellLoading("Отправка заказов...");
ToastNotification.ShowCustom("Отправляем данные", Color.DarkOrange, Color.White); ToastNotification.ShowCustom("Отправляем данные", Color.DarkOrange, Color.White);
@ -848,9 +618,7 @@ namespace Электронная_Фармация
private void tsBtnDownloadData_Click(object sender, EventArgs e) private void tsBtnDownloadData_Click(object sender, EventArgs e)
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
var consigneeSafe = (string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName) string qShowMeCoountDataFromTempOrder = "select count(*) from TempOrderItems";
?.Replace("'", "''");
string qShowMeCoountDataFromTempOrder = $"select count(*) from TempOrderItems where ConsigneeName = '{consigneeSafe}'";
int countDataTempOrder = 0; int countDataTempOrder = 0;
using (SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB)) using (SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
@ -874,8 +642,6 @@ namespace Электронная_Фармация
ClearDocumentTabs(); ClearDocumentTabs();
HF_DownloadDataFromServer hF_DownloadDataFromServer = new HF_DownloadDataFromServer(); HF_DownloadDataFromServer hF_DownloadDataFromServer = new HF_DownloadDataFromServer();
hF_DownloadDataFromServer.ActiveConsigneeName =
string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName;
UiThemeHelper.ApplyToControlTree(hF_DownloadDataFromServer); UiThemeHelper.ApplyToControlTree(hF_DownloadDataFromServer);
DialogResult downloadResult = DialogResult.Cancel; DialogResult downloadResult = DialogResult.Cancel;
if (countDataTempOrder > 0) if (countDataTempOrder > 0)

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Data; using System.Data;
@ -76,8 +76,7 @@ namespace Электронная_Фармация.Forms
[codeConsignees] integer not null, [codeConsignees] integer not null,
[ConsigneesName] nvarchar(128) not null, [ConsigneesName] nvarchar(128) not null,
[ConsigneesAddress] nvarchar(256) not null, [ConsigneesAddress] nvarchar(256) not null,
[ConsigneesUseAsDefault] int(1) not null, [ConsigneesUseAsDefault] int(1) not null
[LocationId] nvarchar(64)
) )
"; ";
@ -251,8 +250,7 @@ create table if not exists [Orders] (
[SummaZakaza] nvarchar(32), [SummaZakaza] nvarchar(32),
[OrderState] nvarchar(32), [OrderState] nvarchar(32),
[Comment] nvarchar(1024), [Comment] nvarchar(1024),
[ConsigneeName] nvarchar(128), [ConsigneeName] nvarchar(128)
[RemoteOrderId] nvarchar(36)
) )
"; ";
@ -309,7 +307,6 @@ create table if not exists [OrderItems] (
string strCheckExistsTableTempOrderItems = @" string strCheckExistsTableTempOrderItems = @"
create table if not exists [TempOrderItems] ( create table if not exists [TempOrderItems] (
[ConsigneeName] nvarchar(128) not null,
[guid_es] nvarchar(36) not null, [guid_es] nvarchar(36) not null,
[es_code] integer not null, [es_code] integer not null,
[supplier_id] nvarchar(36) not null, [supplier_id] nvarchar(36) not null,
@ -321,9 +318,8 @@ create table if not exists [OrderItems] (
[Description] text, [Description] text,
[Zakaz] real, [Zakaz] real,
[SummaZakaza] nvarchar(64), [SummaZakaza] nvarchar(64),
[id_PriceList_Item] integer not null, [id_PriceList_Item] integer primary key not null,
[supplier_price_id] nvarchar(36), [supplier_price_id] nvarchar(36)
primary key ([ConsigneeName], [id_PriceList_Item])
) )
"; ";
@ -396,132 +392,6 @@ create table if not exists [OrderItems] (
cmdCreateNotExistingsTables.CommandText = strCheckExistsTableTempOrderItems; cmdCreateNotExistingsTables.CommandText = strCheckExistsTableTempOrderItems;
cmdCreateNotExistingsTables.ExecuteNonQuery(); cmdCreateNotExistingsTables.ExecuteNonQuery();
// Миграция TempOrderItems под раздельную корзину по аптекам.
// Проверяем наличие ConsigneeName и то, что id_PriceList_Item не является одинарным PK.
bool hasConsigneeName = false;
bool idPriceListItemIsSinglePk = false;
using (var infoCmd = new SQLiteCommand("PRAGMA table_info([TempOrderItems]);", conForCheckTables))
using (var infoReader = infoCmd.ExecuteReader())
{
while (infoReader.Read())
{
var colName = infoReader["name"]?.ToString();
var pk = 0;
try { pk = Convert.ToInt32(infoReader["pk"]); } catch { pk = 0; }
if (string.Equals(colName, "ConsigneeName", StringComparison.OrdinalIgnoreCase))
{
hasConsigneeName = true;
}
if (string.Equals(colName, "id_PriceList_Item", StringComparison.OrdinalIgnoreCase) && pk == 1)
{
idPriceListItemIsSinglePk = true;
}
}
}
if (!hasConsigneeName || idPriceListItemIsSinglePk)
{
string defaultConsignee;
using (var defCmd = new SQLiteCommand(
@"SELECT ifnull((SELECT ConsigneesName
FROM Consignees
WHERE ConsigneesUseAsDefault = 1
LIMIT 1), '')",
conForCheckTables))
{
defaultConsignee = defCmd.ExecuteScalar()?.ToString() ?? string.Empty;
}
using (var tx = conForCheckTables.BeginTransaction())
{
using (var cmd = new SQLiteCommand(conForCheckTables))
{
cmd.Transaction = tx;
cmd.CommandText = @"
CREATE TABLE IF NOT EXISTS [TempOrderItems_new] (
[ConsigneeName] nvarchar(128) not null,
[guid_es] nvarchar(36) not null,
[es_code] integer not null,
[supplier_id] nvarchar(36) not null,
[DrugName] nvarchar(256) not null COLLATE NOCASE,
[SupplierName] nvarchar(256) not null,
[Price] nvarchar(32),
[Quantity] integer,
[ExpiryPeriod] nvarchar(64),
[Description] text,
[Zakaz] real,
[SummaZakaza] nvarchar(64),
[id_PriceList_Item] integer not null,
[supplier_price_id] nvarchar(36),
primary key ([ConsigneeName], [id_PriceList_Item])
);";
cmd.ExecuteNonQuery();
cmd.Parameters.Clear();
cmd.Parameters.AddWithValue("@defaultConsignee", defaultConsignee.Trim());
cmd.CommandText = @"
INSERT INTO [TempOrderItems_new] (
[ConsigneeName],
[guid_es],
[es_code],
[supplier_id],
[DrugName],
[SupplierName],
[Price],
[Quantity],
[ExpiryPeriod],
[Description],
[Zakaz],
[SummaZakaza],
[id_PriceList_Item],
[supplier_price_id]
)
SELECT
@defaultConsignee,
[guid_es],
[es_code],
[supplier_id],
[DrugName],
[SupplierName],
[Price],
[Quantity],
[ExpiryPeriod],
[Description],
[Zakaz],
[SummaZakaza],
[id_PriceList_Item],
[supplier_price_id]
FROM [TempOrderItems];";
cmd.ExecuteNonQuery();
cmd.Parameters.Clear();
cmd.CommandText = @"DROP TABLE [TempOrderItems];";
cmd.ExecuteNonQuery();
cmd.CommandText = @"ALTER TABLE [TempOrderItems_new] RENAME TO [TempOrderItems];";
cmd.ExecuteNonQuery();
}
tx.Commit();
}
}
// LocationId нужен для связи с BuyerLocation на сервере.
TryAddColumn(conForCheckTables, "Consignees", "LocationId", "nvarchar(64)");
// Удаляем старые тестовые грузополучатели — точки создаются в веб-кабинете.
using (var delTest = new SQLiteCommand(
@"DELETE FROM Consignees
WHERE ConsigneesName LIKE 'Тестовая аптека%'
OR ConsigneesAddress LIKE 'Тестовый адрес%';",
conForCheckTables))
{
delTest.ExecuteNonQuery();
}
TryAddColumn(conForCheckTables, "Orders", "ConsigneeName", "nvarchar(128)"); TryAddColumn(conForCheckTables, "Orders", "ConsigneeName", "nvarchar(128)");
TryAddColumn(conForCheckTables, "Invoice", "SupplierName", "nvarchar(256)"); TryAddColumn(conForCheckTables, "Invoice", "SupplierName", "nvarchar(256)");
TryAddColumn(conForCheckTables, "Invoice", "ConsigneesName", "nvarchar(128)"); TryAddColumn(conForCheckTables, "Invoice", "ConsigneesName", "nvarchar(128)");
@ -530,33 +400,12 @@ FROM [TempOrderItems];";
TryAddColumn(conForCheckTables, "Invoice", "SourceOrderId", "integer"); TryAddColumn(conForCheckTables, "Invoice", "SourceOrderId", "integer");
TryAddColumn(conForCheckTables, "Invoice", "SourceOrderNumber", "nvarchar(36)"); TryAddColumn(conForCheckTables, "Invoice", "SourceOrderNumber", "nvarchar(36)");
TryAddColumn(conForCheckTables, "Invoice", "IsRequestedInvoice", "int(1)"); TryAddColumn(conForCheckTables, "Invoice", "IsRequestedInvoice", "int(1)");
TryAddColumn(conForCheckTables, "Orders", "RemoteOrderId", "nvarchar(36)");
TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)"); TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)");
TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)"); TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)");
TryAddColumn(conForCheckTables, "PriceList", "MarkupPercent", "real"); TryAddColumn(conForCheckTables, "PriceList", "MarkupPercent", "real");
TryAddColumn(conForCheckTables, "Consignees", "ClientMarkupPercent", "real");
ConsigneeHelper.MigrateGlobalLocationIdIfNeeded(conForCheckTables);
} }
catch (Exception ex) catch (Exception ex)
{ {
try
{
var logDir = AppConfig.LogsDirectory;
Directory.CreateDirectory(logDir);
var logPath = Path.Combine(
logDir,
$"db-integrity-err-{DateTime.Now:yyyy-MM-dd}.log");
File.AppendAllText(
logPath,
$"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {ex}{Environment.NewLine}{Environment.NewLine}",
Encoding.UTF8);
}
catch
{
// Логирование не должно скрывать исходную ошибку.
}
UiDialogs.ShowError($"Возникла ошибка при проверке локальной базы на целостность.\n{ex}", "Ошибка целостности", this); UiDialogs.ShowError($"Возникла ошибка при проверке локальной базы на целостность.\n{ex}", "Ошибка целостности", this);
} }
finally finally

View File

@ -2,8 +2,15 @@
{ {
partial class FConsignees partial class FConsignees
{ {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null; private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing && (components != null)) if (disposing && (components != null))
@ -15,12 +22,13 @@
#region Windows Form Designer generated code #region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() private void InitializeComponent()
{ {
this.panel1 = new System.Windows.Forms.Panel(); this.panel1 = new System.Windows.Forms.Panel();
this.btnSetDefault = new Elfisa.UI.Controls.ModernButton();
this.btnAdd = new Elfisa.UI.Controls.ModernButton();
this.btnSave = new Elfisa.UI.Controls.ModernButton();
this.txtInteractiveShowMeConsignees = new Elfisa.UI.Controls.ModernTextBox(); this.txtInteractiveShowMeConsignees = new Elfisa.UI.Controls.ModernTextBox();
this.label2 = new System.Windows.Forms.Label(); this.label2 = new System.Windows.Forms.Label();
this.dgvConsignees = new Elfisa.UI.Controls.ModernDataGridView(); this.dgvConsignees = new Elfisa.UI.Controls.ModernDataGridView();
@ -31,67 +39,31 @@
// panel1 // panel1
// //
this.panel1.BackColor = System.Drawing.SystemColors.ButtonHighlight; this.panel1.BackColor = System.Drawing.SystemColors.ButtonHighlight;
this.panel1.Controls.Add(this.btnSetDefault);
this.panel1.Controls.Add(this.btnAdd);
this.panel1.Controls.Add(this.btnSave);
this.panel1.Controls.Add(this.txtInteractiveShowMeConsignees); this.panel1.Controls.Add(this.txtInteractiveShowMeConsignees);
this.panel1.Controls.Add(this.label2); this.panel1.Controls.Add(this.label2);
this.panel1.Controls.Add(this.dgvConsignees); this.panel1.Controls.Add(this.dgvConsignees);
this.panel1.Dock = System.Windows.Forms.DockStyle.Fill; this.panel1.Location = new System.Drawing.Point(13, 14);
this.panel1.Location = new System.Drawing.Point(0, 0); this.panel1.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.panel1.Margin = new System.Windows.Forms.Padding(4);
this.panel1.Name = "panel1"; this.panel1.Name = "panel1";
this.panel1.Padding = new System.Windows.Forms.Padding(12); this.panel1.Size = new System.Drawing.Size(724, 423);
this.panel1.Size = new System.Drawing.Size(820, 480);
this.panel1.TabIndex = 4; this.panel1.TabIndex = 4;
// //
// btnSetDefault
//
this.btnSetDefault.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnSetDefault.Location = new System.Drawing.Point(280, 430);
this.btnSetDefault.Name = "btnSetDefault";
this.btnSetDefault.Size = new System.Drawing.Size(180, 34);
this.btnSetDefault.TabIndex = 6;
this.btnSetDefault.Text = "По умолчанию";
this.btnSetDefault.Click += new System.EventHandler(this.btnSetDefault_Click);
//
// btnAdd
//
this.btnAdd.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnAdd.Location = new System.Drawing.Point(148, 430);
this.btnAdd.Name = "btnAdd";
this.btnAdd.Size = new System.Drawing.Size(120, 34);
this.btnAdd.TabIndex = 5;
this.btnAdd.Text = "Добавить";
this.btnAdd.Click += new System.EventHandler(this.btnAdd_Click);
//
// btnSave
//
this.btnSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.btnSave.Location = new System.Drawing.Point(16, 430);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(120, 34);
this.btnSave.TabIndex = 4;
this.btnSave.Text = "Сохранить";
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// txtInteractiveShowMeConsignees // txtInteractiveShowMeConsignees
// //
this.txtInteractiveShowMeConsignees.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) this.txtInteractiveShowMeConsignees.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
| System.Windows.Forms.AnchorStyles.Right))); this.txtInteractiveShowMeConsignees.Location = new System.Drawing.Point(91, 6);
this.txtInteractiveShowMeConsignees.Font = new System.Drawing.Font("Segoe UI", 11.25F); this.txtInteractiveShowMeConsignees.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.txtInteractiveShowMeConsignees.Location = new System.Drawing.Point(91, 16);
this.txtInteractiveShowMeConsignees.Margin = new System.Windows.Forms.Padding(4);
this.txtInteractiveShowMeConsignees.Name = "txtInteractiveShowMeConsignees"; this.txtInteractiveShowMeConsignees.Name = "txtInteractiveShowMeConsignees";
this.txtInteractiveShowMeConsignees.Size = new System.Drawing.Size(700, 32); this.txtInteractiveShowMeConsignees.Size = new System.Drawing.Size(623, 32);
this.txtInteractiveShowMeConsignees.TabIndex = 3; this.txtInteractiveShowMeConsignees.TabIndex = 3;
this.txtInteractiveShowMeConsignees.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.txtInteractiveShowMeConsignees_KeyPress); this.txtInteractiveShowMeConsignees.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.txtInteractiveShowMeConsignees_KeyPress);
// //
// label2 // label2
// //
this.label2.AutoSize = true; this.label2.AutoSize = true;
this.label2.Font = new System.Drawing.Font("Segoe UI", 12F); this.label2.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
this.label2.Location = new System.Drawing.Point(16, 18); this.label2.Location = new System.Drawing.Point(5, 6);
this.label2.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label2.Name = "label2"; this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(73, 28); this.label2.Size = new System.Drawing.Size(73, 28);
this.label2.TabIndex = 2; this.label2.TabIndex = 2;
@ -101,48 +73,45 @@
// //
this.dgvConsignees.AllowUserToAddRows = false; this.dgvConsignees.AllowUserToAddRows = false;
this.dgvConsignees.AllowUserToDeleteRows = false; this.dgvConsignees.AllowUserToDeleteRows = false;
this.dgvConsignees.AllowUserToResizeColumns = false;
this.dgvConsignees.AllowUserToResizeRows = false; this.dgvConsignees.AllowUserToResizeRows = false;
this.dgvConsignees.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.dgvConsignees.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill; this.dgvConsignees.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.Fill;
this.dgvConsignees.AutoSizeRowsMode = System.Windows.Forms.DataGridViewAutoSizeRowsMode.DisplayedHeaders;
this.dgvConsignees.BackgroundColor = System.Drawing.SystemColors.ButtonFace; this.dgvConsignees.BackgroundColor = System.Drawing.SystemColors.ButtonFace;
this.dgvConsignees.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dgvConsignees.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvConsignees.Location = new System.Drawing.Point(16, 56); this.dgvConsignees.Location = new System.Drawing.Point(11, 44);
this.dgvConsignees.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.dgvConsignees.Name = "dgvConsignees"; this.dgvConsignees.Name = "dgvConsignees";
this.dgvConsignees.ReadOnly = false; this.dgvConsignees.ReadOnly = true;
this.dgvConsignees.RowHeadersWidthSizeMode = System.Windows.Forms.DataGridViewRowHeadersWidthSizeMode.AutoSizeToDisplayedHeaders; this.dgvConsignees.RowHeadersWidthSizeMode = System.Windows.Forms.DataGridViewRowHeadersWidthSizeMode.AutoSizeToDisplayedHeaders;
this.dgvConsignees.Size = new System.Drawing.Size(775, 360); this.dgvConsignees.Size = new System.Drawing.Size(704, 369);
this.dgvConsignees.TabIndex = 1; this.dgvConsignees.TabIndex = 1;
// //
// FConsignees // FConsignees
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(820, 480); this.ClientSize = new System.Drawing.Size(749, 450);
this.Controls.Add(this.panel1); this.Controls.Add(this.panel1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = true; this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
this.MaximizeBox = false;
this.MinimizeBox = false; this.MinimizeBox = false;
this.MinimumSize = new System.Drawing.Size(700, 420);
this.Name = "FConsignees"; this.Name = "FConsignees";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Грузополучатели (аптеки)"; this.Text = "Грузополучатели";
this.Load += new System.EventHandler(this.FConsignees_Load); this.Load += new System.EventHandler(this.FConsignees_Load);
this.panel1.ResumeLayout(false); this.panel1.ResumeLayout(false);
this.panel1.PerformLayout(); this.panel1.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvConsignees)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.dgvConsignees)).EndInit();
this.ResumeLayout(false); this.ResumeLayout(false);
} }
#endregion #endregion
private System.Windows.Forms.Panel panel1; private System.Windows.Forms.Panel panel1;
private Elfisa.UI.Controls.ModernTextBox txtInteractiveShowMeConsignees; private Elfisa.UI.Controls.ModernTextBox txtInteractiveShowMeConsignees;
private System.Windows.Forms.Label label2; private System.Windows.Forms.Label label2;
private Elfisa.UI.Controls.ModernDataGridView dgvConsignees; private Elfisa.UI.Controls.ModernDataGridView dgvConsignees;
private Elfisa.UI.Controls.ModernButton btnSave;
private Elfisa.UI.Controls.ModernButton btnAdd;
private Elfisa.UI.Controls.ModernButton btnSetDefault;
} }
} }

View File

@ -1,43 +1,41 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Data.SQLite; using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Электроннаяармация.Classes; using Электроннаяармация.Classes;
using Электроннаяармация.Properties; using System.Data.SQLite;
namespace Электроннаяармация.Forms namespace Электроннаяармация.Forms
{ {
public partial class FConsignees : Form public partial class FConsignees : Form
{ {
public string SelectedConsigneeNameAfterSave { get; private set; } = string.Empty;
public FConsignees() public FConsignees()
{ {
InitializeComponent(); InitializeComponent();
} }
private async void FConsignees_Load(object sender, EventArgs e) private void FConsignees_Load(object sender, EventArgs e)
{ {
UiThemeHelper.ApplyToControlTree(this); UiThemeHelper.ApplyToControlTree(this);
btnAdd.Text = "С сервера";
showMeContentFromConsigneesTable(); showMeContentFromConsigneesTable();
await SyncFromServerAsync(silent: true);
} }
void showMeContentFromConsigneesTable() void showMeContentFromConsigneesTable()
{ {
using (SQLiteConnection conConsignees = new SQLiteConnection(AppConfig.SqliteConnectionString)) using (SQLiteConnection conConsignees = new SQLiteConnection(AppConfig.SqliteConnectionString))
{ {
string selectFromConsignees = @" string selectFromConsignees = $@"
select [idConsignees] as [Id], select [codeConsignees] as [Код грузополучателя],
[codeConsignees] as [Код],
[ConsigneesName] as [Наименование], [ConsigneesName] as [Наименование],
[ConsigneesAddress] as [Адрес], [ConsigneesAddress] as [Адрес],
ifnull([LocationId], '') as [Location ID], [ConsigneesUseAsDefault] as [По умолчанию?]
[ConsigneesUseAsDefault] as [По умолчанию]
from [Consignees] from [Consignees]
order by [ConsigneesUseAsDefault] desc, [ConsigneesName]"; order by [ConsigneesUseAsDefault] desc";
try try
{ {
conConsignees.Open(); conConsignees.Open();
@ -46,21 +44,16 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
SQLiteDataAdapter daConsigneeList = new SQLiteDataAdapter(cmdShowConsigneesList); SQLiteDataAdapter daConsigneeList = new SQLiteDataAdapter(cmdShowConsigneesList);
daConsigneeList.Fill(tableConsignees); daConsigneeList.Fill(tableConsignees);
dgvConsignees.DataSource = tableConsignees; dgvConsignees.DataSource = tableConsignees;
if (dgvConsignees.Columns.Contains("Id")) dgvConsignees.Columns[3].ValueType = typeof(float);
{
dgvConsignees.Columns["Id"].Visible = false;
}
foreach (DataGridViewColumn column in dgvConsignees.Columns)
{
// Редактируем только адрес локально; Location ID и имя приходят с сервера.
column.ReadOnly = column.Name != "Адрес";
}
} }
catch (Exception ex) catch (Exception ex)
{ {
MessageBox.Show($"Возникла ошибка при заполнении данных о грузополучателях.\nТекст ошибки:\n{ex}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show($"Возникла ошибка при заполнении данных о грузополучателях.\nТекст ошибки:\n{ex}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
finally
{
conConsignees.Close();
}
} }
} }
@ -68,9 +61,9 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
{ {
if (e.KeyChar == (char)Keys.Enter) if (e.KeyChar == (char)Keys.Enter)
{ {
if (dgvConsignees.Rows.Count > 0 && dgvConsignees.DataSource is DataTable table) if (dgvConsignees.Rows.Count > 0)
{ {
table.DefaultView.RowFilter = $"[Наименование] like '%{txtInteractiveShowMeConsignees.Text.Replace("'", "''")}%'"; (dgvConsignees.DataSource as DataTable).DefaultView.RowFilter = string.Format($"[Наименование] like '%{txtInteractiveShowMeConsignees.Text}%'");
if (dgvConsignees.Rows.Count == 0) if (dgvConsignees.Rows.Count == 0)
{ {
showMeContentFromConsigneesTable(); showMeContentFromConsigneesTable();
@ -84,132 +77,5 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
} }
} }
private void btnSave_Click(object sender, EventArgs e)
{
if (!(dgvConsignees.DataSource is DataTable table))
{
return;
}
dgvConsignees.EndEdit();
var selectedName = dgvConsignees.CurrentRow?.Cells["Наименование"]?.Value?.ToString()?.Trim()
?? string.Empty;
SelectedConsigneeNameAfterSave = selectedName;
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{
connection.Open();
using (var tx = connection.BeginTransaction())
{
foreach (DataRow row in table.Rows)
{
if (row.RowState == DataRowState.Unchanged)
{
continue;
}
using (var cmd = new SQLiteCommand(
@"UPDATE Consignees
SET ConsigneesAddress = @address
WHERE idConsignees = @id",
connection,
tx))
{
cmd.Parameters.AddWithValue("@address", row["Адрес"]?.ToString() ?? string.Empty);
cmd.Parameters.AddWithValue("@id", Convert.ToInt64(row["Id"]));
cmd.ExecuteNonQuery();
}
}
tx.Commit();
}
}
table.AcceptChanges();
ToastNotification.ShowSuccess("Грузополучатели сохранены");
if (!string.IsNullOrWhiteSpace(selectedName))
{
ConsigneeHelper.SetActiveConsignee(selectedName);
}
DialogResult = DialogResult.OK;
Close();
}
private async void btnAdd_Click(object sender, EventArgs e)
{
await SyncFromServerAsync(silent: false);
}
private async Task SyncFromServerAsync(bool silent)
{
try
{
btnAdd.Enabled = false;
var client = new ApiClient();
if (!client.IsAuthenticated)
{
var token = Settings.Default.stringToken;
if (string.IsNullOrWhiteSpace(token))
{
if (!silent)
{
UiDialogs.ShowInfo(
"Сначала войдите на сервер — грузополучатели подтягиваются из веб-кабинета покупателя.",
"Грузополучатели",
this);
}
return;
}
client.SetToken(token);
}
var locations = await client.GetBuyerLocationsAsync();
var count = ConsigneeHelper.SyncFromServer(locations);
showMeContentFromConsigneesTable();
if (!silent)
{
ToastNotification.ShowSuccess(count > 0
? $"Обновлено грузополучателей: {count}"
: "На сервере пока нет грузополучателей");
}
}
catch (Exception ex)
{
if (!silent)
{
UiDialogs.ShowError($"Не удалось загрузить грузополучателей с сервера.\n{ex.Message}", "Грузополучатели", this);
}
else
{
AppDebugLog.Error("FConsignees", "Тихая синхронизация грузополучателей не удалась", ex);
}
}
finally
{
btnAdd.Enabled = true;
}
}
private void btnSetDefault_Click(object sender, EventArgs e)
{
if (dgvConsignees.CurrentRow == null)
{
UiDialogs.ShowInfo("Выберите аптеку в списке.", "Грузополучатели", this);
return;
}
var name = dgvConsignees.CurrentRow.Cells["Наименование"]?.Value?.ToString()?.Trim();
if (string.IsNullOrWhiteSpace(name))
{
return;
}
ConsigneeHelper.SetActiveConsignee(name);
showMeContentFromConsigneesTable();
ToastNotification.ShowSuccess($"Аптека по умолчанию: {name}");
}
} }
} }

View File

@ -2,8 +2,15 @@
{ {
partial class FDebug partial class FDebug
{ {
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null; private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing && (components != null)) if (disposing && (components != null))
@ -15,267 +22,115 @@
#region Windows Form Designer generated code #region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent() private void InitializeComponent()
{ {
this.splitMain = new System.Windows.Forms.SplitContainer();
this.listTemplates = new System.Windows.Forms.ListBox();
this.lblTemplates = new System.Windows.Forms.Label();
this.splitRight = new System.Windows.Forms.SplitContainer();
this.panelTop = new System.Windows.Forms.Panel();
this.btnScanParams = new Elfisa.UI.Controls.ModernButton();
this.btnExecuteSelectSqlScript = new Elfisa.UI.Controls.ModernButton();
this.btnExecuteSqlScript = new Elfisa.UI.Controls.ModernButton();
this.btnCleanSqlScript = new Elfisa.UI.Controls.ModernButton();
this.lblDbPath = new System.Windows.Forms.Label();
this.label1 = new System.Windows.Forms.Label();
this.RTXT_Sql = new System.Windows.Forms.RichTextBox(); this.RTXT_Sql = new System.Windows.Forms.RichTextBox();
this.lblParams = new System.Windows.Forms.Label(); this.label1 = new System.Windows.Forms.Label();
this.panelParams = new System.Windows.Forms.Panel(); this.btnCleanSqlScript = new Elfisa.UI.Controls.ModernButton();
this.btnExecuteSqlScript = new Elfisa.UI.Controls.ModernButton();
this.btnExecuteSelectSqlScript = new Elfisa.UI.Controls.ModernButton();
this.dgvSelectCommand = new Elfisa.UI.Controls.ModernDataGridView(); this.dgvSelectCommand = new Elfisa.UI.Controls.ModernDataGridView();
this.lblStatus = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.splitMain)).BeginInit();
this.splitMain.Panel1.SuspendLayout();
this.splitMain.Panel2.SuspendLayout();
this.splitMain.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitRight)).BeginInit();
this.splitRight.Panel1.SuspendLayout();
this.splitRight.Panel2.SuspendLayout();
this.splitRight.SuspendLayout();
this.panelTop.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvSelectCommand)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.dgvSelectCommand)).BeginInit();
this.SuspendLayout(); this.SuspendLayout();
// //
// splitMain // RTXT_Sql
// //
this.splitMain.Dock = System.Windows.Forms.DockStyle.Fill; this.RTXT_Sql.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
this.splitMain.Location = new System.Drawing.Point(0, 0);
this.splitMain.Name = "splitMain";
//
// splitMain.Panel1
//
this.splitMain.Panel1.Controls.Add(this.listTemplates);
this.splitMain.Panel1.Controls.Add(this.lblTemplates);
this.splitMain.Panel1MinSize = 180;
//
// splitMain.Panel2
//
this.splitMain.Panel2.Controls.Add(this.splitRight);
this.splitMain.Size = new System.Drawing.Size(980, 620);
this.splitMain.SplitterDistance = 240;
this.splitMain.TabIndex = 0;
//
// listTemplates
//
this.listTemplates.Dock = System.Windows.Forms.DockStyle.Fill;
this.listTemplates.Font = new System.Drawing.Font("Segoe UI", 10F);
this.listTemplates.FormattingEnabled = true;
this.listTemplates.ItemHeight = 23;
this.listTemplates.Location = new System.Drawing.Point(0, 28);
this.listTemplates.Name = "listTemplates";
this.listTemplates.Size = new System.Drawing.Size(240, 592);
this.listTemplates.TabIndex = 1;
this.listTemplates.SelectedIndexChanged += new System.EventHandler(this.listTemplates_SelectedIndexChanged);
//
// lblTemplates
//
this.lblTemplates.Dock = System.Windows.Forms.DockStyle.Top;
this.lblTemplates.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold);
this.lblTemplates.Location = new System.Drawing.Point(0, 0);
this.lblTemplates.Name = "lblTemplates";
this.lblTemplates.Padding = new System.Windows.Forms.Padding(8, 4, 0, 0);
this.lblTemplates.Size = new System.Drawing.Size(240, 28);
this.lblTemplates.TabIndex = 0;
this.lblTemplates.Text = "Шаблоны SQL";
//
// splitRight
//
this.splitRight.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitRight.Location = new System.Drawing.Point(0, 0);
this.splitRight.Name = "splitRight";
this.splitRight.Orientation = System.Windows.Forms.Orientation.Horizontal;
//
// splitRight.Panel1
//
this.splitRight.Panel1.Controls.Add(this.RTXT_Sql);
this.splitRight.Panel1.Controls.Add(this.panelParams);
this.splitRight.Panel1.Controls.Add(this.lblParams);
this.splitRight.Panel1.Controls.Add(this.panelTop);
this.splitRight.Panel1MinSize = 220;
//
// splitRight.Panel2
//
this.splitRight.Panel2.Controls.Add(this.dgvSelectCommand);
this.splitRight.Panel2.Controls.Add(this.lblStatus);
this.splitRight.Panel2MinSize = 120;
this.splitRight.Size = new System.Drawing.Size(736, 620);
this.splitRight.SplitterDistance = 320;
this.splitRight.TabIndex = 0;
//
// panelTop
//
this.panelTop.Controls.Add(this.btnScanParams);
this.panelTop.Controls.Add(this.btnExecuteSelectSqlScript);
this.panelTop.Controls.Add(this.btnExecuteSqlScript);
this.panelTop.Controls.Add(this.btnCleanSqlScript);
this.panelTop.Controls.Add(this.lblDbPath);
this.panelTop.Controls.Add(this.label1);
this.panelTop.Dock = System.Windows.Forms.DockStyle.Top;
this.panelTop.Location = new System.Drawing.Point(0, 0);
this.panelTop.Name = "panelTop";
this.panelTop.Size = new System.Drawing.Size(736, 72);
this.panelTop.TabIndex = 0;
//
// btnScanParams
//
this.btnScanParams.Location = new System.Drawing.Point(430, 36);
this.btnScanParams.Name = "btnScanParams";
this.btnScanParams.Size = new System.Drawing.Size(150, 28);
this.btnScanParams.TabIndex = 5;
this.btnScanParams.Text = "Параметры из SQL";
this.btnScanParams.Click += new System.EventHandler(this.btnScanParams_Click);
//
// btnExecuteSelectSqlScript
//
this.btnExecuteSelectSqlScript.Location = new System.Drawing.Point(232, 36);
this.btnExecuteSelectSqlScript.Name = "btnExecuteSelectSqlScript";
this.btnExecuteSelectSqlScript.Size = new System.Drawing.Size(182, 28);
this.btnExecuteSelectSqlScript.TabIndex = 4;
this.btnExecuteSelectSqlScript.Text = "Выполнить SELECT";
this.btnExecuteSelectSqlScript.Click += new System.EventHandler(this.btnExecuteSelectSqlScript_Click);
//
// btnExecuteSqlScript
//
this.btnExecuteSqlScript.Location = new System.Drawing.Point(116, 36);
this.btnExecuteSqlScript.Name = "btnExecuteSqlScript";
this.btnExecuteSqlScript.Size = new System.Drawing.Size(100, 28);
this.btnExecuteSqlScript.TabIndex = 3;
this.btnExecuteSqlScript.Text = "Выполнить";
this.btnExecuteSqlScript.Click += new System.EventHandler(this.btnExecuteSqlScript_Click);
//
// btnCleanSqlScript
//
this.btnCleanSqlScript.Location = new System.Drawing.Point(10, 36);
this.btnCleanSqlScript.Name = "btnCleanSqlScript";
this.btnCleanSqlScript.Size = new System.Drawing.Size(100, 28);
this.btnCleanSqlScript.TabIndex = 2;
this.btnCleanSqlScript.Text = "Очистить";
this.btnCleanSqlScript.Click += new System.EventHandler(this.btnCleanSqlScript_Click);
//
// lblDbPath
//
this.lblDbPath.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right))); | System.Windows.Forms.AnchorStyles.Right)));
this.lblDbPath.Location = new System.Drawing.Point(100, 8); this.RTXT_Sql.Location = new System.Drawing.Point(9, 70);
this.lblDbPath.Name = "lblDbPath"; this.RTXT_Sql.Margin = new System.Windows.Forms.Padding(4);
this.lblDbPath.Size = new System.Drawing.Size(620, 20); this.RTXT_Sql.Name = "RTXT_Sql";
this.lblDbPath.TabIndex = 1; this.RTXT_Sql.Size = new System.Drawing.Size(565, 118);
this.lblDbPath.Text = "db"; this.RTXT_Sql.TabIndex = 0;
this.RTXT_Sql.Text = "";
// //
// label1 // label1
// //
this.label1.AutoSize = true; this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(8, 8); this.label1.Location = new System.Drawing.Point(6, 50);
this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.label1.Name = "label1"; this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(84, 16); this.label1.Size = new System.Drawing.Size(84, 16);
this.label1.TabIndex = 0; this.label1.TabIndex = 1;
this.label1.Text = "SQL / база:"; this.label1.Text = "SQL-запрос";
// //
// RTXT_Sql // btnCleanSqlScript
// //
this.RTXT_Sql.Dock = System.Windows.Forms.DockStyle.Fill; this.btnCleanSqlScript.Location = new System.Drawing.Point(9, 15);
this.RTXT_Sql.Font = new System.Drawing.Font("Consolas", 10F); this.btnCleanSqlScript.Margin = new System.Windows.Forms.Padding(4);
this.RTXT_Sql.Location = new System.Drawing.Point(0, 100); this.btnCleanSqlScript.Name = "btnCleanSqlScript";
this.RTXT_Sql.Name = "RTXT_Sql"; this.btnCleanSqlScript.Size = new System.Drawing.Size(100, 28);
this.RTXT_Sql.Size = new System.Drawing.Size(736, 120); this.btnCleanSqlScript.TabIndex = 2;
this.RTXT_Sql.TabIndex = 3; this.btnCleanSqlScript.Text = "Очистить";
this.RTXT_Sql.Text = ""; this.btnCleanSqlScript.UseVisualStyleBackColor = true;
this.btnCleanSqlScript.Click += new System.EventHandler(this.btnCleanSqlScript_Click);
// //
// lblParams // btnExecuteSqlScript
// //
this.lblParams.Dock = System.Windows.Forms.DockStyle.Top; this.btnExecuteSqlScript.Location = new System.Drawing.Point(128, 15);
this.lblParams.Location = new System.Drawing.Point(0, 72); this.btnExecuteSqlScript.Margin = new System.Windows.Forms.Padding(4);
this.lblParams.Name = "lblParams"; this.btnExecuteSqlScript.Name = "btnExecuteSqlScript";
this.lblParams.Padding = new System.Windows.Forms.Padding(8, 4, 0, 0); this.btnExecuteSqlScript.Size = new System.Drawing.Size(100, 28);
this.lblParams.Size = new System.Drawing.Size(736, 28); this.btnExecuteSqlScript.TabIndex = 3;
this.lblParams.TabIndex = 1; this.btnExecuteSqlScript.Text = "Выполнить";
this.lblParams.Text = "Параметры (@name / {{name}})"; this.btnExecuteSqlScript.UseVisualStyleBackColor = true;
this.btnExecuteSqlScript.Click += new System.EventHandler(this.btnExecuteSqlScript_Click);
// //
// panelParams // btnExecuteSelectSqlScript
// //
this.panelParams.AutoScroll = true; this.btnExecuteSelectSqlScript.Location = new System.Drawing.Point(254, 15);
this.panelParams.Dock = System.Windows.Forms.DockStyle.Bottom; this.btnExecuteSelectSqlScript.Margin = new System.Windows.Forms.Padding(4);
this.panelParams.Location = new System.Drawing.Point(0, 220); this.btnExecuteSelectSqlScript.Name = "btnExecuteSelectSqlScript";
this.panelParams.Name = "panelParams"; this.btnExecuteSelectSqlScript.Size = new System.Drawing.Size(182, 28);
this.panelParams.Size = new System.Drawing.Size(736, 100); this.btnExecuteSelectSqlScript.TabIndex = 4;
this.panelParams.TabIndex = 2; this.btnExecuteSelectSqlScript.Text = "Выполнить SELECT";
this.btnExecuteSelectSqlScript.UseVisualStyleBackColor = true;
this.btnExecuteSelectSqlScript.Click += new System.EventHandler(this.btnExecuteSelectSqlScript_Click);
// //
// dgvSelectCommand // dgvSelectCommand
// //
this.dgvSelectCommand.AllowUserToAddRows = false; this.dgvSelectCommand.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
this.dgvSelectCommand.AllowUserToDeleteRows = false; | System.Windows.Forms.AnchorStyles.Left)
this.dgvSelectCommand.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.DisplayedCells; | System.Windows.Forms.AnchorStyles.Right)));
this.dgvSelectCommand.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dgvSelectCommand.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvSelectCommand.Dock = System.Windows.Forms.DockStyle.Fill; this.dgvSelectCommand.Location = new System.Drawing.Point(9, 195);
this.dgvSelectCommand.Location = new System.Drawing.Point(0, 0);
this.dgvSelectCommand.Name = "dgvSelectCommand"; this.dgvSelectCommand.Name = "dgvSelectCommand";
this.dgvSelectCommand.ReadOnly = true;
this.dgvSelectCommand.RowHeadersWidth = 51; this.dgvSelectCommand.RowHeadersWidth = 51;
this.dgvSelectCommand.RowTemplate.Height = 24; this.dgvSelectCommand.RowTemplate.Height = 24;
this.dgvSelectCommand.Size = new System.Drawing.Size(736, 268); this.dgvSelectCommand.Size = new System.Drawing.Size(562, 150);
this.dgvSelectCommand.TabIndex = 0; this.dgvSelectCommand.TabIndex = 5;
//
// lblStatus
//
this.lblStatus.Dock = System.Windows.Forms.DockStyle.Bottom;
this.lblStatus.Location = new System.Drawing.Point(0, 268);
this.lblStatus.Name = "lblStatus";
this.lblStatus.Padding = new System.Windows.Forms.Padding(8, 4, 8, 4);
this.lblStatus.Size = new System.Drawing.Size(736, 28);
this.lblStatus.TabIndex = 1;
this.lblStatus.Text = "Готово";
// //
// FDebug // FDebug
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(980, 620); this.ClientSize = new System.Drawing.Size(583, 351);
this.Controls.Add(this.splitMain); this.Controls.Add(this.dgvSelectCommand);
this.MinimumSize = new System.Drawing.Size(800, 500); this.Controls.Add(this.btnExecuteSelectSqlScript);
this.Controls.Add(this.btnExecuteSqlScript);
this.Controls.Add(this.btnCleanSqlScript);
this.Controls.Add(this.label1);
this.Controls.Add(this.RTXT_Sql);
this.Margin = new System.Windows.Forms.Padding(4);
this.Name = "FDebug"; this.Name = "FDebug";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; this.Text = "FDebug";
this.Text = "SQL-отладка";
this.Load += new System.EventHandler(this.FDebug_Load);
this.splitMain.Panel1.ResumeLayout(false);
this.splitMain.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitMain)).EndInit();
this.splitMain.ResumeLayout(false);
this.splitRight.Panel1.ResumeLayout(false);
this.splitRight.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitRight)).EndInit();
this.splitRight.ResumeLayout(false);
this.panelTop.ResumeLayout(false);
this.panelTop.PerformLayout();
((System.ComponentModel.ISupportInitialize)(this.dgvSelectCommand)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.dgvSelectCommand)).EndInit();
this.ResumeLayout(false); this.ResumeLayout(false);
this.PerformLayout();
} }
#endregion #endregion
private System.Windows.Forms.SplitContainer splitMain;
private System.Windows.Forms.ListBox listTemplates;
private System.Windows.Forms.Label lblTemplates;
private System.Windows.Forms.SplitContainer splitRight;
private System.Windows.Forms.Panel panelTop;
private Elfisa.UI.Controls.ModernButton btnScanParams;
private Elfisa.UI.Controls.ModernButton btnExecuteSelectSqlScript;
private Elfisa.UI.Controls.ModernButton btnExecuteSqlScript;
private Elfisa.UI.Controls.ModernButton btnCleanSqlScript;
private System.Windows.Forms.Label lblDbPath;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.RichTextBox RTXT_Sql; private System.Windows.Forms.RichTextBox RTXT_Sql;
private System.Windows.Forms.Label lblParams; private System.Windows.Forms.Label label1;
private System.Windows.Forms.Panel panelParams; private Elfisa.UI.Controls.ModernButton btnCleanSqlScript;
private Elfisa.UI.Controls.ModernButton btnExecuteSqlScript;
private Elfisa.UI.Controls.ModernButton btnExecuteSelectSqlScript;
private Elfisa.UI.Controls.ModernDataGridView dgvSelectCommand; private Elfisa.UI.Controls.ModernDataGridView dgvSelectCommand;
private System.Windows.Forms.Label lblStatus;
} }
} }

View File

@ -1,385 +1,81 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Data.SQLite;
using System.Diagnostics;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Elfisa.UI.Theming;
using Электроннаяармация.Classes; using Электроннаяармация.Classes;
using System.Data.SQLite;
namespace Электроннаяармация.Forms namespace Электроннаяармация.Forms
{ {
public partial class FDebug : Form public partial class FDebug : Form
{ {
private sealed class SqlTemplate
{
public string Title { get; set; }
public string Sql { get; set; }
public string[] Parameters { get; set; }
}
private readonly List<SqlTemplate> _templates = new List<SqlTemplate>();
private readonly Dictionary<string, TextBox> _paramBoxes = new Dictionary<string, TextBox>(StringComparer.OrdinalIgnoreCase);
private readonly Timer _highlightTimer = new Timer();
private bool _highlighting;
public FDebug() public FDebug()
{ {
InitializeComponent(); InitializeComponent();
_highlightTimer.Interval = 180;
_highlightTimer.Tick += (_, __) =>
{
_highlightTimer.Stop();
ApplySqlHighlight();
};
} }
private void FDebug_Load(object sender, EventArgs e) private void btnExecuteSqlScript_Click(object sender, EventArgs e)
{ {
UiThemeHelper.ApplyToControlTree(this); string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
Text = "SQL-отладка";
lblDbPath.Text = AppConfig.SqliteDbPath;
RTXT_Sql.DetectUrls = false;
RTXT_Sql.AcceptsTab = true;
RTXT_Sql.TextChanged += RTXT_Sql_TextChanged;
BuildTemplates();
listTemplates.Items.Clear();
foreach (var template in _templates)
{
listTemplates.Items.Add(template.Title);
}
SetStatus("Готово. Выберите шаблон или введите свой SQL."); string sqlScript = RTXT_Sql.Text;
ApplySqlHighlight();
FormClosed += (_, __) =>
{
_highlightTimer.Stop();
_highlightTimer.Dispose();
};
}
private void RTXT_Sql_TextChanged(object sender, EventArgs e)
{
if (_highlighting)
{
return;
}
_highlightTimer.Stop(); using (SQLiteConnection debugCon = new SQLiteConnection(connectionStringToLocalDB))
_highlightTimer.Start();
}
private void ApplySqlHighlight()
{ {
if (_highlighting || RTXT_Sql == null || RTXT_Sql.IsDisposed)
{
return;
}
_highlighting = true;
try try
{ {
SqlSyntaxHighlighter.Apply(RTXT_Sql, ThemeManager.IsDarkProductivityActive); debugCon.Open();
SQLiteCommand cmdDebug = new SQLiteCommand(sqlScript, debugCon);
cmdDebug.ExecuteNonQuery();
}
catch (Exception ex)
{
MessageBox.Show($"При выполнении запроса возникла ошибка. Текст ошибки:\n{ex.ToString()}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
finally finally
{ {
_highlighting = false; debugCon.Close();
} }
} }
private void BuildTemplates()
{
_templates.Clear();
_templates.Add(new SqlTemplate
{
Title = "Счётчик прайса",
Sql = "SELECT COUNT(*) AS [Позиций] FROM PriceList;"
});
_templates.Add(new SqlTemplate
{
Title = "Прайс по поставщикам",
Sql = @"SELECT SupplierName AS [Поставщик],
COUNT(*) AS [Позиций],
MIN(Price) AS [МинЦена],
MAX(Price) AS [МаксЦена]
FROM PriceList
GROUP BY SupplierName
ORDER BY COUNT(*) DESC;"
});
_templates.Add(new SqlTemplate
{
Title = "Заказы по статусам",
Sql = @"SELECT OrderState AS [Статус], COUNT(*) AS [Кол-во]
FROM Orders
GROUP BY OrderState
ORDER BY COUNT(*) DESC;"
});
_templates.Add(new SqlTemplate
{
Title = "Грузополучатели + LocationId",
Sql = @"SELECT codeConsignees AS [Код],
ConsigneesName AS [Аптека],
ConsigneesAddress AS [Адрес],
ifnull(LocationId, '') AS [LocationId],
ConsigneesUseAsDefault AS [ПоУмолчанию]
FROM Consignees
ORDER BY ConsigneesUseAsDefault DESC, ConsigneesName;"
});
_templates.Add(new SqlTemplate
{
Title = "Корзина TempOrderItems",
Sql = @"SELECT COUNT(*) AS [Строк],
ifnull(SUM(CAST(SummaZakaza AS REAL)), 0) AS [Сумма]
FROM TempOrderItems;"
});
_templates.Add(new SqlTemplate
{
Title = "Поиск по наименованию",
Sql = @"SELECT DrugName, SupplierName, Price, Quantity, supplier_price_id
FROM PriceList
WHERE DrugName LIKE '%' || @q || '%'
ORDER BY DrugName
LIMIT 200;",
Parameters = new[] { "q" }
});
_templates.Add(new SqlTemplate
{
Title = "Последние заказы",
Sql = @"SELECT Id_Order, OrderNumber, OrderDate, SupplierName, ConsigneeName, OrderState, SummaZakaza
FROM Orders
ORDER BY Id_Order DESC
LIMIT 50;"
});
_templates.Add(new SqlTemplate
{
Title = "Список таблиц",
Sql = @"SELECT name AS [Таблица], type AS [Тип]
FROM sqlite_master
WHERE type IN ('table', 'view')
ORDER BY name;"
});
_templates.Add(new SqlTemplate
{
Title = "Позиции заказа",
Sql = @"SELECT oi.Id_Order, oi.DrugName, oi.Zakaz, oi.Price, oi.supplier_price_id, o.ConsigneeName, o.OrderState
FROM OrderItems oi
INNER JOIN Orders o ON o.Id_Order = oi.Id_Order
WHERE oi.Id_Order = @orderId
ORDER BY oi.DrugName;",
Parameters = new[] { "orderId" }
});
_templates.Add(new SqlTemplate
{
Title = "Очистить корзину (DML)",
Sql = "DELETE FROM TempOrderItems;"
});
}
private void listTemplates_SelectedIndexChanged(object sender, EventArgs e)
{
if (listTemplates.SelectedIndex < 0 || listTemplates.SelectedIndex >= _templates.Count)
{
return;
}
var template = _templates[listTemplates.SelectedIndex];
RTXT_Sql.Text = template.Sql;
RebuildParamEditors(template.Parameters);
SetStatus($"Шаблон: {template.Title}");
}
private void RebuildParamEditors(string[] parameters)
{
panelParams.Controls.Clear();
_paramBoxes.Clear();
if (parameters == null || parameters.Length == 0)
{
var empty = new Label
{
Text = "Параметры не требуются. Можно писать {{name}} или @name в SQL.",
AutoSize = true,
MaximumSize = new Size(panelParams.Width - 20, 0),
Location = new Point(8, 8)
};
panelParams.Controls.Add(empty);
return;
}
int y = 8;
foreach (var name in parameters)
{
var lbl = new Label
{
Text = "@" + name,
AutoSize = true,
Location = new Point(8, y + 4)
};
var box = new TextBox
{
Name = "param_" + name,
Width = Math.Max(180, panelParams.Width - 140),
Location = new Point(120, y)
};
panelParams.Controls.Add(lbl);
panelParams.Controls.Add(box);
_paramBoxes[name] = box;
y += 34;
}
} }
private void btnCleanSqlScript_Click(object sender, EventArgs e) private void btnCleanSqlScript_Click(object sender, EventArgs e)
{ {
RTXT_Sql.Text = string.Empty; RTXT_Sql.Text = "";
dgvSelectCommand.DataSource = null;
RebuildParamEditors(null);
SetStatus("Очищено.");
}
private void btnExecuteSqlScript_Click(object sender, EventArgs e)
{
ExecuteSql(forceSelect: false);
} }
private void btnExecuteSelectSqlScript_Click(object sender, EventArgs e) private void btnExecuteSelectSqlScript_Click(object sender, EventArgs e)
{ {
ExecuteSql(forceSelect: true); string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
}
private void ExecuteSql(bool forceSelect) string sqlScript = RTXT_Sql.Text;
{
var sql = (RTXT_Sql.Text ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(sql))
{
SetStatus("Пустой SQL.");
return;
}
sql = ApplyMustachePlaceholders(sql); using (SQLiteConnection debugSelectCon = new SQLiteConnection(connectionStringToLocalDB))
bool isSelect = forceSelect || LooksLikeSelect(sql);
var sw = Stopwatch.StartNew();
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
{ {
try try
{ {
connection.Open(); debugSelectCon.Open();
using (var command = new SQLiteCommand(sql, connection)) SQLiteCommand cmdSelectSqlCommand = new SQLiteCommand(sqlScript, debugSelectCon);
{ SQLiteDataAdapter daSelectDataAdapter = new SQLiteDataAdapter(cmdSelectSqlCommand);
BindParameters(command, sql); DataTable tableSelect = new DataTable();
daSelectDataAdapter.Fill(tableSelect);
if (isSelect) dgvSelectCommand.DataSource = tableSelect;
{
using (var adapter = new SQLiteDataAdapter(command))
{
var table = new DataTable();
adapter.Fill(table);
dgvSelectCommand.DataSource = table;
sw.Stop();
SetStatus($"SELECT: {table.Rows.Count} строк за {sw.ElapsedMilliseconds} мс");
}
}
else
{
int affected = command.ExecuteNonQuery();
dgvSelectCommand.DataSource = null;
sw.Stop();
SetStatus($"Выполнено. Затронуто строк: {affected}. {sw.ElapsedMilliseconds} мс");
}
}
} }
catch(Exception ex) catch(Exception ex)
{ {
sw.Stop(); MessageBox.Show($"Возникла ошибка при заполнении таблицы данными.\nТекст ошибки:\n{ex.ToString()}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
SetStatus("Ошибка: " + ex.Message);
MessageBox.Show($"При выполнении запроса возникла ошибка.\n{ex}", "SQL-отладка", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
} finally
}
private static bool LooksLikeSelect(string sql)
{ {
var trimmed = sql.TrimStart(); debugSelectCon.Close();
return trimmed.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)
|| trimmed.StartsWith("PRAGMA", StringComparison.OrdinalIgnoreCase)
|| trimmed.StartsWith("WITH", StringComparison.OrdinalIgnoreCase)
|| trimmed.StartsWith("EXPLAIN", StringComparison.OrdinalIgnoreCase);
}
private string ApplyMustachePlaceholders(string sql)
{
return Regex.Replace(sql, @"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}", match =>
{
var name = match.Groups[1].Value;
if (_paramBoxes.TryGetValue(name, out var box))
{
return (box.Text ?? string.Empty).Replace("'", "''");
}
return match.Value;
});
}
private void BindParameters(SQLiteCommand command, string sql)
{
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Match match in Regex.Matches(sql, @"@([A-Za-z_][A-Za-z0-9_]*)"))
{
names.Add(match.Groups[1].Value);
}
foreach (var name in names)
{
object value = string.Empty;
if (_paramBoxes.TryGetValue(name, out var box))
{
value = box.Text ?? string.Empty;
}
command.Parameters.AddWithValue("@" + name, value);
}
// Параметры из панели, даже если в SQL их ещё нет (удобно для шаблонов).
foreach (var pair in _paramBoxes)
{
if (!names.Contains(pair.Key))
{
command.Parameters.AddWithValue("@" + pair.Key, pair.Value.Text ?? string.Empty);
} }
} }
} }
private void SetStatus(string text)
{
lblStatus.Text = text;
}
private void btnScanParams_Click(object sender, EventArgs e)
{
var sql = RTXT_Sql.Text ?? string.Empty;
var names = Regex.Matches(sql, @"@([A-Za-z_][A-Za-z0-9_]*)")
.Cast<Match>()
.Select(m => m.Groups[1].Value)
.Concat(Regex.Matches(sql, @"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}")
.Cast<Match>()
.Select(m => m.Groups[1].Value))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
var existing = _paramBoxes.ToDictionary(p => p.Key, p => p.Value.Text, StringComparer.OrdinalIgnoreCase);
RebuildParamEditors(names);
foreach (var pair in existing)
{
if (_paramBoxes.TryGetValue(pair.Key, out var box))
{
box.Text = pair.Value;
}
}
SetStatus(names.Length == 0 ? "Параметры в SQL не найдены." : $"Найдено параметров: {names.Length}");
}
} }
} }

View File

@ -45,14 +45,14 @@
this.dgvConsignees.BackgroundColor = System.Drawing.SystemColors.ButtonFace; this.dgvConsignees.BackgroundColor = System.Drawing.SystemColors.ButtonFace;
this.dgvConsignees.BorderStyle = System.Windows.Forms.BorderStyle.None; this.dgvConsignees.BorderStyle = System.Windows.Forms.BorderStyle.None;
this.dgvConsignees.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize; this.dgvConsignees.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.dgvConsignees.Location = new System.Drawing.Point(16, 56); this.dgvConsignees.Location = new System.Drawing.Point(12, 56);
this.dgvConsignees.MultiSelect = false; this.dgvConsignees.MultiSelect = false;
this.dgvConsignees.Name = "dgvConsignees"; this.dgvConsignees.Name = "dgvConsignees";
this.dgvConsignees.ReadOnly = true; this.dgvConsignees.ReadOnly = true;
this.dgvConsignees.RowHeadersVisible = false; this.dgvConsignees.RowHeadersVisible = false;
this.dgvConsignees.RowHeadersWidth = 51; this.dgvConsignees.RowHeadersWidth = 51;
this.dgvConsignees.RowTemplate.Height = 24; this.dgvConsignees.RowTemplate.Height = 24;
this.dgvConsignees.Size = new System.Drawing.Size(640, 295); this.dgvConsignees.Size = new System.Drawing.Size(409, 295);
this.dgvConsignees.TabIndex = 0; this.dgvConsignees.TabIndex = 0;
// //
// label1 // label1
@ -69,7 +69,7 @@
// //
this.btnConfirmConsignee.FlatStyle = System.Windows.Forms.FlatStyle.Popup; this.btnConfirmConsignee.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnConfirmConsignee.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204))); this.btnConfirmConsignee.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
this.btnConfirmConsignee.Location = new System.Drawing.Point(567, 361); this.btnConfirmConsignee.Location = new System.Drawing.Point(336, 361);
this.btnConfirmConsignee.Name = "btnConfirmConsignee"; this.btnConfirmConsignee.Name = "btnConfirmConsignee";
this.btnConfirmConsignee.Size = new System.Drawing.Size(89, 43); this.btnConfirmConsignee.Size = new System.Drawing.Size(89, 43);
this.btnConfirmConsignee.TabIndex = 2; this.btnConfirmConsignee.TabIndex = 2;
@ -81,7 +81,7 @@
// //
this.btnCloseThis.FlatStyle = System.Windows.Forms.FlatStyle.Popup; this.btnCloseThis.FlatStyle = System.Windows.Forms.FlatStyle.Popup;
this.btnCloseThis.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204))); this.btnCloseThis.Font = new System.Drawing.Font("Segoe UI Semibold", 10.8F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
this.btnCloseThis.Location = new System.Drawing.Point(420, 361); this.btnCloseThis.Location = new System.Drawing.Point(174, 361);
this.btnCloseThis.Name = "btnCloseThis"; this.btnCloseThis.Name = "btnCloseThis";
this.btnCloseThis.Size = new System.Drawing.Size(125, 43); this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
this.btnCloseThis.TabIndex = 3; this.btnCloseThis.TabIndex = 3;
@ -93,7 +93,7 @@
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(672, 412); this.ClientSize = new System.Drawing.Size(434, 412);
this.Controls.Add(this.btnCloseThis); this.Controls.Add(this.btnCloseThis);
this.Controls.Add(this.btnConfirmConsignee); this.Controls.Add(this.btnConfirmConsignee);
this.Controls.Add(this.label1); this.Controls.Add(this.label1);

View File

@ -1,16 +1,21 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data; using System.Data;
using System.Data.SQLite; using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms; using System.Windows.Forms;
using Электроннаяармация;
using Электроннаяармация.Classes; using Электроннаяармация.Classes;
using System.Data.SQLite;
using Электроннаяармация;
namespace Электроннаяармация.HelpForms namespace Электроннаяармация.HelpForms
{ {
public partial class HFConsignees : Form public partial class HFConsignees : Form
{ {
public new ElectroPharmacy ParentForm { get; set; } public ElectroPharmacy ParentForm { get; set; }
public HFConsignees() public HFConsignees()
{ {
InitializeComponent(); InitializeComponent();
@ -26,39 +31,28 @@ namespace Электронная_Фармация.HelpForms
{ {
conConsigneesList.Open(); conConsigneesList.Open();
string qShowMeConsigneesList = @" string qShowMeConsigneesList = "select ConsigneesName as [Грузополучатель] from Consignees order by ConsigneesUseAsDefault desc";
select ConsigneesName as [Грузополучатель],
ConsigneesAddress as [Адрес]
from Consignees
order by ConsigneesUseAsDefault desc, ConsigneesName";
SQLiteCommand cmdShowMeConsigneesList = new SQLiteCommand(qShowMeConsigneesList, conConsigneesList); SQLiteCommand cmdShowMeConsigneesList = new SQLiteCommand(qShowMeConsigneesList, conConsigneesList);
SQLiteDataAdapter daConsigneesList = new SQLiteDataAdapter(cmdShowMeConsigneesList); SQLiteDataAdapter daConsigneesList = new SQLiteDataAdapter(cmdShowMeConsigneesList);
DataTable tableConsignees = new DataTable(); DataTable tableConsignees = new DataTable();
daConsigneesList.Fill(tableConsignees); daConsigneesList.Fill(tableConsignees);
dgvConsignees.DataSource = tableConsignees; dgvConsignees.DataSource = tableConsignees;
if (dgvConsignees.Columns.Contains("Грузополучатель"))
{
dgvConsignees.Columns["Грузополучатель"].FillWeight = 45;
}
if (dgvConsignees.Columns.Contains("Адрес"))
{
dgvConsignees.Columns["Адрес"].FillWeight = 55;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
UiDialogs.ShowError($"При загрузке списка получателей возникла ошибка.\n{ex}", "Ошибка", this); UiDialogs.ShowError($"При загрузке списка получателей возникла ошибка.\n{ex}", "Ошибка", this);
} }
finally
{
conConsigneesList.Close();
}
} }
} }
private void btnCloseThis_Click(object sender, EventArgs e) private void btnCloseThis_Click(object sender, EventArgs e)
{ {
DialogResult = DialogResult.Cancel; this.Close();
Close();
} }
private void btnConfirmConsignee_Click(object sender, EventArgs e) private void btnConfirmConsignee_Click(object sender, EventArgs e)
@ -69,27 +63,7 @@ order by ConsigneesUseAsDefault desc, ConsigneesName";
return; return;
} }
string consigneesName = dgvConsignees.CurrentRow.Cells[0].Value?.ToString(); string consigneesName = dgvConsignees.CurrentRow.Cells[0].Value.ToString();
if (string.IsNullOrWhiteSpace(consigneesName))
{
UiDialogs.ShowInfo("Выберите грузополучателя из списка.", "Грузополучатель", this);
return;
}
var locationId = ConsigneeHelper.GetLocationIdByName(consigneesName);
if (string.IsNullOrWhiteSpace(locationId))
{
var ask = UiDialogs.ConfirmYesNo(
$"У аптеки «{consigneesName}» не задан Location ID.\nПродолжить без него? Заказы этой аптеки нельзя будет отправить, пока ID не заполните.",
"Location ID",
this);
if (ask != DialogResult.Yes)
{
return;
}
}
ConsigneeHelper.SetActiveConsignee(consigneesName);
ParentForm?.showMePriceList(consigneesName); ParentForm?.showMePriceList(consigneesName);
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();

View File

@ -43,26 +43,23 @@ using Электронная_Фармация.Classes;
using (SQLiteConnection conAddComment = new SQLiteConnection(connectionStringToLocalDB)) using (SQLiteConnection conAddComment = new SQLiteConnection(connectionStringToLocalDB))
{ {
string qShowMeStatusOrder = $"select OrderState from Orders where Id_Order = '{_idOrder}'";
string qAddComment = $"update Orders set Comment = '{rtxtComment.Text}' where Id_Order = '{_idOrder}'";
try try
{ {
conAddComment.Open(); conAddComment.Open();
string orderState; SQLiteCommand cmdShowMeOrderStatus = new SQLiteCommand(qShowMeStatusOrder, conAddComment);
using (var cmdShowMeOrderStatus = new SQLiteCommand("select OrderState from Orders where Id_Order = @id", conAddComment))
{
cmdShowMeOrderStatus.Parameters.AddWithValue("@id", _idOrder);
orderState = Convert.ToString(cmdShowMeOrderStatus.ExecuteScalar()) ?? string.Empty;
}
if (orderState == "НОВЫЙ") string OrderState = cmdShowMeOrderStatus.ExecuteScalar().ToString();
if (OrderState == "НОВЫЙ")
{ {
using (var cmdAddComment = new SQLiteCommand("update Orders set Comment = @comment where Id_Order = @id", conAddComment)) SQLiteCommand cmdAddComment = new SQLiteCommand(qAddComment, conAddComment);
{
cmdAddComment.Parameters.AddWithValue("@comment", rtxtComment.Text);
cmdAddComment.Parameters.AddWithValue("@id", _idOrder);
cmdAddComment.ExecuteNonQuery(); cmdAddComment.ExecuteNonQuery();
} }
}
else else
{ {
UiDialogs.ShowError("Добавить комментарий можно только к заказу со статусом НОВЫЙ.", "Ошибка", this); UiDialogs.ShowError("Добавить комментарий можно только к заказу со статусом НОВЫЙ.", "Ошибка", this);

View File

@ -24,10 +24,6 @@ namespace Электронная_Фармация.HelpForms
InitializeComponent(); InitializeComponent();
} }
// Чтобы при обновлении прайса не терять корзины других аптек,
// чистим TempOrderItems только для активного грузополучателя.
public string ActiveConsigneeName { get; set; } = string.Empty;
private readonly string _login = Settings.Default.stringLogin ?? string.Empty; private readonly string _login = Settings.Default.stringLogin ?? string.Empty;
private readonly string _password = Settings.Default.stringPassword ?? string.Empty; private readonly string _password = Settings.Default.stringPassword ?? string.Empty;
private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString; private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString;
@ -54,29 +50,15 @@ namespace Электронная_Фармация.HelpForms
SetOverallProgress(1, "Авторизация..."); SetOverallProgress(1, "Авторизация...");
await EnsureAuthenticatedAsync(client); await EnsureAuthenticatedAsync(client);
try
{
AppendStatus("Загружаю грузополучателей с сервера...");
var locations = await client.GetBuyerLocationsAsync();
var synced = ConsigneeHelper.SyncFromServer(locations);
AppendStatus(synced > 0
? $"Грузополучателей обновлено: {synced}"
: "Грузополучателей на сервере пока нет");
}
catch (Exception locEx)
{
AppendStatus($"Грузополучатели не обновлены: {locEx.Message}");
AppDebugLog.Error("Download", "Не удалось синхронизировать грузополучателей", locEx);
}
SetOverallProgress(3, "Загрузка прайса с сервера..."); SetOverallProgress(3, "Загрузка прайса с сервера...");
AppendStatus("Загружаю сводный прайс..."); AppendStatus("Загружаю сводный прайс...");
var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client);
int totalItems = allSuppliersSummary.Summary?.Count ?? 0; int totalItems = allSuppliersSummary.Summary?.Count ?? 0;
ApplyClientMarkupSettings(allSuppliersSummary);
// Наценки прайса/региона/покупателя считает сервер. if (allSuppliersSummary.MarkupPercent.HasValue)
// Десктоп сохраняет цену из API как есть, без локального пересчёта. {
AppendStatus("Цена берётся с сервера без локального пересчёта наценок"); AppendStatus($"Наценка клиента: {allSuppliersSummary.MarkupPercent.Value:0.##}%");
}
AppendStatus($"Получено {totalItems} позиций"); AppendStatus($"Получено {totalItems} позиций");
SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)"); SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)");
@ -105,10 +87,6 @@ namespace Электронная_Фармация.HelpForms
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу."); AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций"); ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
Settings.Default.LastPriceUpdateUtc = DateTime.UtcNow;
Settings.Default.LastPriceReminderUtc = DateTime.MinValue;
Settings.Default.Save();
} }
catch (UnauthorizedAccessException ex) catch (UnauthorizedAccessException ex)
{ {
@ -171,182 +149,27 @@ namespace Электронная_Фармация.HelpForms
? null ? null
: Settings.Default.RegionId; : Settings.Default.RegionId;
const int supplierPageLimit = 100000; // Один крупный запрос (OFFSET на больших страницах валит сервер).
const int discoveryLimit = 50000; const int pageSize = 200000;
const int maxPages = 100;
var combined = new PriceSummaryResponse { Summary = new List<PriceSummaryItem>() }; var combined = new PriceSummaryResponse { Summary = new List<PriceSummaryItem>() };
var transferProgress = new Progress<HttpTransferProgress>(ReportTransferProgress); var transferProgress = new Progress<HttpTransferProgress>(ReportTransferProgress);
var supplierIds = new List<string>(ConsigneeHelper.GetKnownSupplierIds()); for (int page = 0; page < maxPages; page++)
if (supplierIds.Count == 0)
{ {
AppendStatus("Первая загрузка: определяю список поставщиков..."); int offset = page * pageSize;
SetOverallProgress(3, "Определение поставщиков...");
var discovery = await GetPriceSummaryWithAuthRetryAsync(
client,
supplierId: null,
regionId: regionId,
limit: discoveryLimit,
offset: 0,
transferProgress: transferProgress);
if (discovery?.MarkupPercent.HasValue == true)
{
combined.MarkupPercent = discovery.MarkupPercent;
}
var discovered = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (discovery?.Summary != null)
{
foreach (var item in discovery.Summary)
{
if (!string.IsNullOrWhiteSpace(item.SupplierId))
{
discovered.Add(item.SupplierId.Trim());
}
}
}
supplierIds.AddRange(discovered);
AppendStatus($"Найдено поставщиков: {supplierIds.Count}");
}
if (supplierIds.Count == 0)
{
AppendStatus("Поставщики не найдены, пробую полный запрос...");
var fallback = await GetPriceSummaryWithAuthRetryAsync(
client,
supplierId: null,
regionId: regionId,
limit: 200000,
offset: 0,
transferProgress: transferProgress);
if (fallback?.MarkupPercent.HasValue == true)
{
combined.MarkupPercent = fallback.MarkupPercent;
}
if (fallback?.Summary != null)
{
combined.Summary.AddRange(fallback.Summary);
}
return combined;
}
int totalSuppliers = supplierIds.Count;
for (int i = 0; i < totalSuppliers; i++)
{
var supplierId = supplierIds[i];
int supplierProgressStart = 3 + (int)((DownloadPhaseEnd - 3) * ((double)i / totalSuppliers));
int supplierProgressEnd = 3 + (int)((DownloadPhaseEnd - 3) * ((double)(i + 1) / totalSuppliers));
SetOverallProgress( SetOverallProgress(
supplierProgressStart, Math.Min(DownloadPhaseEnd - 1, 3 + page),
$"Поставщик {i + 1}/{totalSuppliers}..."); page == 0 ? "Скачивание прайса с сервера..." : $"Скачивание страницы {page + 1}...");
AppendStatus($"Загрузка поставщика {i + 1}/{totalSuppliers}: {supplierId}");
PriceSummaryResponse chunk; PriceSummaryResponse chunk;
try try
{ {
chunk = await GetPriceSummaryWithAuthRetryAsync( chunk = await client.GetPriceSummaryAsync(
client,
supplierId: supplierId,
regionId: regionId,
limit: supplierPageLimit,
offset: 0,
transferProgress: transferProgress);
}
catch (Exception ex)
{
AppendStatus($"Ошибка поставщика {supplierId}: {ex.Message}");
AppDebugLog.Error("Download", $"Ошибка загрузки прайса поставщика {supplierId}", ex);
continue;
}
if (i == 0 && chunk?.MarkupPercent.HasValue == true && !combined.MarkupPercent.HasValue)
{
combined.MarkupPercent = chunk.MarkupPercent;
}
int got = chunk?.Summary?.Count ?? 0;
if (got > 0)
{
combined.Summary.AddRange(chunk.Summary);
}
SetOverallProgress(
Math.Max(supplierProgressStart, supplierProgressEnd - 1),
$"Поставщик {i + 1}/{totalSuppliers}: +{got}, всего {combined.Summary.Count}");
AppendStatus($"Поставщик {i + 1}/{totalSuppliers}: получено {got}, всего {combined.Summary.Count}");
}
// Лёгкий discovery новых поставщиков, которых ещё не было в локальной базе.
try
{
AppendStatus("Проверка новых поставщиков...");
var probe = await GetPriceSummaryWithAuthRetryAsync(
client,
supplierId: null, supplierId: null,
regionId: regionId, regionId: regionId,
limit: 5000, limit: pageSize,
offset: 0,
transferProgress: null);
var known = new HashSet<string>(supplierIds, StringComparer.OrdinalIgnoreCase);
var newcomers = new List<string>();
if (probe?.Summary != null)
{
foreach (var item in probe.Summary)
{
if (!string.IsNullOrWhiteSpace(item.SupplierId) && known.Add(item.SupplierId.Trim()))
{
newcomers.Add(item.SupplierId.Trim());
}
}
}
for (int i = 0; i < newcomers.Count; i++)
{
var supplierId = newcomers[i];
AppendStatus($"Новый поставщик {i + 1}/{newcomers.Count}: {supplierId}");
var chunk = await GetPriceSummaryWithAuthRetryAsync(
client,
supplierId: supplierId,
regionId: regionId,
limit: supplierPageLimit,
offset: 0,
transferProgress: transferProgress);
int got = chunk?.Summary?.Count ?? 0;
if (got > 0)
{
combined.Summary.AddRange(chunk.Summary);
}
AppendStatus($"Новый поставщик: +{got}, всего {combined.Summary.Count}");
}
}
catch (Exception ex)
{
AppendStatus($"Проверка новых поставщиков пропущена: {ex.Message}");
AppDebugLog.Error("Download", "Ошибка discovery новых поставщиков", ex);
}
return combined;
}
private async Task<PriceSummaryResponse> GetPriceSummaryWithAuthRetryAsync(
ApiClient client,
string supplierId,
string regionId,
int limit,
int offset,
IProgress<HttpTransferProgress> transferProgress)
{
try
{
return await client.GetPriceSummaryAsync(
supplierId: supplierId,
regionId: regionId,
limit: limit,
offset: offset, offset: offset,
transferProgress: transferProgress); transferProgress: transferProgress);
} }
@ -358,13 +181,47 @@ namespace Электронная_Фармация.HelpForms
Settings.Default.Save(); Settings.Default.Save();
client.SetToken(null); client.SetToken(null);
await LoginAndSaveTokenAsync(client); await LoginAndSaveTokenAsync(client);
return await client.GetPriceSummaryAsync( SetOverallProgress(3, "Повторная загрузка прайса...");
supplierId: supplierId, chunk = await client.GetPriceSummaryAsync(
supplierId: null,
regionId: regionId, regionId: regionId,
limit: limit, limit: pageSize,
offset: offset, offset: offset,
transferProgress: transferProgress); transferProgress: transferProgress);
} }
int got = chunk?.Summary?.Count ?? 0;
if (page == 0 && chunk?.MarkupPercent.HasValue == true)
{
combined.MarkupPercent = chunk.MarkupPercent;
}
if (got > 0)
{
combined.Summary.AddRange(chunk.Summary);
AppendStatus($"Загружено {combined.Summary.Count} позиций...");
}
if (got < pageSize)
{
break;
}
}
return combined;
}
private static void ApplyClientMarkupSettings(PriceSummaryResponse response)
{
if (response?.MarkupPercent == null)
{
return;
}
var markup = response.MarkupPercent.Value;
Settings.Default.ClientMarkupPercent = markup;
Settings.Default.IntDefaultPercentMarkup = (int)Math.Round(markup, MidpointRounding.AwayFromZero);
Settings.Default.Save();
} }
private void ReportTransferProgress(HttpTransferProgress transfer) private void ReportTransferProgress(HttpTransferProgress transfer)
@ -435,6 +292,7 @@ namespace Электронная_Фармация.HelpForms
return tablePrice; return tablePrice;
} }
var clientMarkup = allSuppliersSummary.MarkupPercent;
int total = allSuppliersSummary.Summary.Count; int total = allSuppliersSummary.Summary.Count;
int current = 0; int current = 0;
foreach (var item in allSuppliersSummary.Summary) foreach (var item in allSuppliersSummary.Summary)
@ -446,10 +304,7 @@ namespace Электронная_Фармация.HelpForms
row["supplier_id"] = item.SupplierId ?? string.Empty; row["supplier_id"] = item.SupplierId ?? string.Empty;
row["DrugName"] = item.DrugName ?? string.Empty; row["DrugName"] = item.DrugName ?? string.Empty;
row["SupplierName"] = item.SupplierName ?? string.Empty; row["SupplierName"] = item.SupplierName ?? string.Empty;
row["Price"] = item.Price?.ToString() ?? string.Empty;
// Цена уже итоговая с сервера — локально не пересчитываем.
var serverPrice = item.Price ?? 0m;
row["Price"] = serverPrice.ToString(System.Globalization.CultureInfo.InvariantCulture);
row["Quantity"] = item.Quantity?.ToString() ?? string.Empty; row["Quantity"] = item.Quantity?.ToString() ?? string.Empty;
row["ExpiryPeriod"] = item.ExpiryPeriod == null ? (object)DBNull.Value : item.ExpiryPeriod; row["ExpiryPeriod"] = item.ExpiryPeriod == null ? (object)DBNull.Value : item.ExpiryPeriod;
row["Description"] = item.Description == null ? (object)DBNull.Value : item.Description; row["Description"] = item.Description == null ? (object)DBNull.Value : item.Description;
@ -457,9 +312,8 @@ namespace Электронная_Фармация.HelpForms
row["SummaZakaza"] = string.Empty; row["SummaZakaza"] = string.Empty;
row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim(); row["TradeName"] = string.IsNullOrWhiteSpace(item.TradeName) ? (object)DBNull.Value : item.TradeName.Trim();
row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim(); row["Dosage"] = string.IsNullOrWhiteSpace(item.Dosage) ? (object)DBNull.Value : item.Dosage.Trim();
row["MarkupPercent"] = item.MarkupPercent.HasValue && item.MarkupPercent.Value > 0m var markupPercent = MarkupHelper.ResolveMarkupPercent(item.MarkupPercent, clientMarkup, Settings.Default.ClientMarkupPercent);
? item.MarkupPercent.Value row["MarkupPercent"] = markupPercent;
: 0m;
tablePrice.Rows.Add(row); tablePrice.Rows.Add(row);
current++; current++;
@ -475,9 +329,7 @@ namespace Электронная_Фармация.HelpForms
private void SavePriceTable(DataTable tablePrice, IProgress<CountProgress> progress = null) private void SavePriceTable(DataTable tablePrice, IProgress<CountProgress> progress = null)
{ {
const string commandToDelete = "delete from [PriceList]"; const string commandToDelete = "delete from [PriceList]";
var commandToDeleteTempOrder = string.IsNullOrWhiteSpace(ActiveConsigneeName) const string commandToDeleteTempOrder = "delete from [TempOrderItems]";
? "delete from [TempOrderItems]"
: "delete from [TempOrderItems] where [ConsigneeName] = @consignee";
const string commandToInsert = @"INSERT INTO [PriceList] ( const string commandToInsert = @"INSERT INTO [PriceList] (
[guid_es], [guid_es],
[es_code], [es_code],
@ -523,58 +375,33 @@ VALUES
using (var cmdDeleteTempOrder = new SQLiteCommand(commandToDeleteTempOrder, sqliteCon)) using (var cmdDeleteTempOrder = new SQLiteCommand(commandToDeleteTempOrder, sqliteCon))
{ {
cmdDeleteBeforeInsert.ExecuteNonQuery(); cmdDeleteBeforeInsert.ExecuteNonQuery();
if (!string.IsNullOrWhiteSpace(ActiveConsigneeName) &&
commandToDeleteTempOrder.Contains("@consignee"))
{
cmdDeleteTempOrder.Parameters.AddWithValue("@consignee", ActiveConsigneeName.Trim());
}
cmdDeleteTempOrder.ExecuteNonQuery(); cmdDeleteTempOrder.ExecuteNonQuery();
} }
using (var transaction = sqliteCon.BeginTransaction()) using (var transaction = sqliteCon.BeginTransaction())
using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction))
{ {
var pGuidEs = cmd.Parameters.Add("@guid_es", DbType.String);
var pEsCode = cmd.Parameters.Add("@es_code", DbType.String);
var pSupplierId = cmd.Parameters.Add("@supplier_id", DbType.String);
var pDrugName = cmd.Parameters.Add("@DrugName", DbType.String);
var pSupplierName = cmd.Parameters.Add("@SupplierName", DbType.String);
var pPrice = cmd.Parameters.Add("@Price", DbType.String);
var pQuantity = cmd.Parameters.Add("@Quantity", DbType.String);
var pExpiry = cmd.Parameters.Add("@ExpiryPeriod", DbType.String);
var pDescription = cmd.Parameters.Add("@Description", DbType.String);
var pZakaz = cmd.Parameters.Add("@Zakaz", DbType.String);
var pSumma = cmd.Parameters.Add("@SummaZakaza", DbType.String);
var pSupplierPriceId = cmd.Parameters.Add("@supplier_price_id", DbType.String);
var pTradeName = cmd.Parameters.Add("@TradeName", DbType.String);
var pDosage = cmd.Parameters.Add("@Dosage", DbType.String);
var pMarkup = cmd.Parameters.Add("@MarkupPercent", DbType.Object);
int saved = 0; int saved = 0;
bool hasTradeName = tablePrice.Columns.Contains("TradeName");
bool hasDosage = tablePrice.Columns.Contains("Dosage");
bool hasMarkup = tablePrice.Columns.Contains("MarkupPercent");
foreach (DataRow row in tablePrice.Rows) foreach (DataRow row in tablePrice.Rows)
{ {
pGuidEs.Value = row["guid_es"] ?? DBNull.Value; using (var cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction))
pEsCode.Value = row["es_code"] ?? DBNull.Value; {
pSupplierId.Value = row["supplier_id"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@guid_es", row["guid_es"]);
pDrugName.Value = row["DrugName"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@es_code", row["es_code"]);
pSupplierName.Value = row["SupplierName"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@supplier_id", row["supplier_id"]);
pPrice.Value = row["Price"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@DrugName", row["DrugName"]);
pQuantity.Value = row["Quantity"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@SupplierName", row["SupplierName"]);
pExpiry.Value = row["ExpiryPeriod"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@Price", row["Price"]);
pDescription.Value = row["Description"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@Quantity", row["Quantity"]);
pZakaz.Value = row["Zakaz"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@ExpiryPeriod", row["ExpiryPeriod"]);
pSumma.Value = row["SummaZakaza"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@Description", row["Description"]);
pSupplierPriceId.Value = row["supplier_price_id"] ?? DBNull.Value; cmd.Parameters.AddWithValue("@Zakaz", row["Zakaz"]);
pTradeName.Value = hasTradeName ? (row["TradeName"] ?? DBNull.Value) : DBNull.Value; cmd.Parameters.AddWithValue("@SummaZakaza", row["SummaZakaza"]);
pDosage.Value = hasDosage ? (row["Dosage"] ?? DBNull.Value) : DBNull.Value; cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]);
pMarkup.Value = hasMarkup ? (row["MarkupPercent"] ?? DBNull.Value) : DBNull.Value; cmd.Parameters.AddWithValue("@TradeName", row.Table.Columns.Contains("TradeName") ? row["TradeName"] : DBNull.Value);
cmd.Parameters.AddWithValue("@Dosage", row.Table.Columns.Contains("Dosage") ? row["Dosage"] : DBNull.Value);
cmd.Parameters.AddWithValue("@MarkupPercent", row.Table.Columns.Contains("MarkupPercent") ? row["MarkupPercent"] : DBNull.Value);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
}
saved++; saved++;
if (progress != null && (saved % 250 == 0 || saved == total)) if (progress != null && (saved % 250 == 0 || saved == total))
@ -756,5 +583,3 @@ VALUES
} }
} }
} }

View File

@ -55,58 +55,40 @@ namespace Электронная_Фармация.HelpForms
this.lblHint.Size = new System.Drawing.Size(420, 48); this.lblHint.Size = new System.Drawing.Size(420, 48);
this.lblHint.TabIndex = 1; this.lblHint.TabIndex = 1;
this.lblHint.Text = "Укажите UUID торговой точки. Он будет использоваться при отправке заказов на сервер."; this.lblHint.Text = "Укажите UUID торговой точки. Он будет использоваться при отправке заказов на сервер.";
// lblConsigneeName
this.lblConsigneeName.AutoSize = true;
this.lblConsigneeName.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold);
this.lblConsigneeName.Location = new System.Drawing.Point(20, 128);
this.lblConsigneeName.Name = "lblConsigneeName";
this.lblConsigneeName.Size = new System.Drawing.Size(0, 0);
this.lblConsigneeName.TabIndex = 2;
this.lblConsigneeName.Text = "";
// lblConsigneeAddress
this.lblConsigneeAddress.AutoSize = true;
this.lblConsigneeAddress.Font = new System.Drawing.Font("Segoe UI", 10F);
this.lblConsigneeAddress.Location = new System.Drawing.Point(20, 150);
this.lblConsigneeAddress.Name = "lblConsigneeAddress";
this.lblConsigneeAddress.Size = new System.Drawing.Size(0, 0);
this.lblConsigneeAddress.TabIndex = 3;
this.lblConsigneeAddress.Text = "";
// //
// lblLocationId // lblLocationId
// //
this.lblLocationId.AutoSize = true; this.lblLocationId.AutoSize = true;
this.lblLocationId.Font = new System.Drawing.Font("Segoe UI", 12F); this.lblLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
this.lblLocationId.Location = new System.Drawing.Point(20, 182); this.lblLocationId.Location = new System.Drawing.Point(20, 128);
this.lblLocationId.Name = "lblLocationId"; this.lblLocationId.Name = "lblLocationId";
this.lblLocationId.Size = new System.Drawing.Size(108, 28); this.lblLocationId.Size = new System.Drawing.Size(108, 28);
this.lblLocationId.TabIndex = 4; this.lblLocationId.TabIndex = 2;
this.lblLocationId.Text = "Location ID"; this.lblLocationId.Text = "Location ID";
// //
// txtLocationId // txtLocationId
// //
this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F); this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
this.txtLocationId.Location = new System.Drawing.Point(24, 214); this.txtLocationId.Location = new System.Drawing.Point(24, 160);
this.txtLocationId.Name = "txtLocationId"; this.txtLocationId.Name = "txtLocationId";
this.txtLocationId.Size = new System.Drawing.Size(412, 34); this.txtLocationId.Size = new System.Drawing.Size(412, 34);
this.txtLocationId.TabIndex = 5; this.txtLocationId.TabIndex = 3;
// //
// btnCancel // btnCancel
// //
this.btnCancel.Location = new System.Drawing.Point(24, 275); this.btnCancel.Location = new System.Drawing.Point(24, 220);
this.btnCancel.Name = "btnCancel"; this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(125, 40); this.btnCancel.Size = new System.Drawing.Size(125, 40);
this.btnCancel.TabIndex = 6; this.btnCancel.TabIndex = 4;
this.btnCancel.Text = "Отмена"; this.btnCancel.Text = "Отмена";
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click); this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
// //
// btnSave // btnSave
// //
this.btnSave.Location = new System.Drawing.Point(221, 275); this.btnSave.Location = new System.Drawing.Point(221, 220);
this.btnSave.Name = "btnSave"; this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(215, 40); this.btnSave.Size = new System.Drawing.Size(215, 40);
this.btnSave.TabIndex = 7; this.btnSave.TabIndex = 5;
this.btnSave.Text = "Сохранить"; this.btnSave.Text = "Сохранить";
this.btnSave.Click += new System.EventHandler(this.btnSave_Click); this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
// //
@ -116,12 +98,10 @@ namespace Электронная_Фармация.HelpForms
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.btnCancel; this.CancelButton = this.btnCancel;
this.ClientSize = new System.Drawing.Size(460, 330); this.ClientSize = new System.Drawing.Size(460, 280);
this.Controls.Add(this.btnSave); this.Controls.Add(this.btnSave);
this.Controls.Add(this.btnCancel); this.Controls.Add(this.btnCancel);
this.Controls.Add(this.txtLocationId); this.Controls.Add(this.txtLocationId);
this.Controls.Add(this.lblConsigneeAddress);
this.Controls.Add(this.lblConsigneeName);
this.Controls.Add(this.lblLocationId); this.Controls.Add(this.lblLocationId);
this.Controls.Add(this.lblHint); this.Controls.Add(this.lblHint);
this.Controls.Add(this.panelHeader); this.Controls.Add(this.panelHeader);
@ -143,8 +123,6 @@ namespace Электронная_Фармация.HelpForms
private System.Windows.Forms.Panel panelHeader; private System.Windows.Forms.Panel panelHeader;
private System.Windows.Forms.Label lblTitle; private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblHint; private System.Windows.Forms.Label lblHint;
private System.Windows.Forms.Label lblConsigneeName;
private System.Windows.Forms.Label lblConsigneeAddress;
private System.Windows.Forms.Label lblLocationId; private System.Windows.Forms.Label lblLocationId;
private Elfisa.UI.Controls.ModernTextBox txtLocationId; private Elfisa.UI.Controls.ModernTextBox txtLocationId;
private Elfisa.UI.Controls.ModernButton btnCancel; private Elfisa.UI.Controls.ModernButton btnCancel;

View File

@ -10,9 +10,6 @@ namespace Электронная_Фармация.HelpForms
{ {
public partial class HF_LocationId : Form public partial class HF_LocationId : Form
{ {
private string _consigneeName = string.Empty;
private string _consigneeAddress = string.Empty;
public HF_LocationId() public HF_LocationId()
{ {
InitializeComponent(); InitializeComponent();
@ -31,20 +28,6 @@ namespace Электронная_Фармация.HelpForms
} }
} }
/// <summary>
/// Показывает Location ID с контекстом: выбранная аптека (имя/адрес).
/// </summary>
public static bool PromptAndSave(IWin32Window owner, string consigneeName, string consigneeAddress)
{
using (var form = new HF_LocationId())
{
form._consigneeName = consigneeName ?? string.Empty;
form._consigneeAddress = consigneeAddress ?? string.Empty;
UiThemeHelper.ApplyToControlTree(form);
return form.ShowDialog(owner) == DialogResult.OK;
}
}
private void HF_LocationId_Load(object sender, EventArgs e) private void HF_LocationId_Load(object sender, EventArgs e)
{ {
UiThemeHelper.ApplyToControlTree(this); UiThemeHelper.ApplyToControlTree(this);
@ -52,18 +35,6 @@ namespace Электронная_Фармация.HelpForms
txtLocationId.Text = Settings.Default.LocationId ?? string.Empty; txtLocationId.Text = Settings.Default.LocationId ?? string.Empty;
txtLocationId.SelectAll(); txtLocationId.SelectAll();
txtLocationId.Focus(); txtLocationId.Focus();
if (lblConsigneeName != null)
{
lblConsigneeName.Text = _consigneeName;
lblConsigneeName.Visible = !string.IsNullOrWhiteSpace(_consigneeName);
}
if (lblConsigneeAddress != null)
{
lblConsigneeAddress.Text = _consigneeAddress;
lblConsigneeAddress.Visible = !string.IsNullOrWhiteSpace(_consigneeAddress);
}
} }
private void btnSave_Click(object sender, EventArgs e) private void btnSave_Click(object sender, EventArgs e)

View File

@ -38,6 +38,11 @@
this.checkTokenGained = new System.Windows.Forms.CheckBox(); this.checkTokenGained = new System.Windows.Forms.CheckBox();
this.panel1 = new System.Windows.Forms.Panel(); this.panel1 = new System.Windows.Forms.Panel();
this.label3 = new System.Windows.Forms.Label(); this.label3 = new System.Windows.Forms.Label();
this.label5 = new System.Windows.Forms.Label();
this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox();
this.labelExportPath = new System.Windows.Forms.Label();
this.txtInvoiceExportPath = new Elfisa.UI.Controls.ModernTextBox();
this.btnBrowseExportPath = new Elfisa.UI.Controls.ModernButton();
this.panel1.SuspendLayout(); this.panel1.SuspendLayout();
this.SuspendLayout(); this.SuspendLayout();
// //
@ -77,9 +82,55 @@
this.txtPassword.Size = new System.Drawing.Size(375, 34); this.txtPassword.Size = new System.Drawing.Size(375, 34);
this.txtPassword.TabIndex = 3; this.txtPassword.TabIndex = 3;
// //
// label5
//
this.label5.AutoSize = true;
this.label5.Font = new System.Drawing.Font("Segoe UI", 12F);
this.label5.Location = new System.Drawing.Point(23, 225);
this.label5.Name = "label5";
this.label5.Size = new System.Drawing.Size(103, 28);
this.label5.TabIndex = 11;
this.label5.Text = "URL API";
//
// txtApiUrl
//
this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 12F);
this.txtApiUrl.Location = new System.Drawing.Point(132, 225);
this.txtApiUrl.Name = "txtApiUrl";
this.txtApiUrl.Size = new System.Drawing.Size(271, 34);
this.txtApiUrl.TabIndex = 12;
//
// labelExportPath
//
this.labelExportPath.AutoSize = true;
this.labelExportPath.Font = new System.Drawing.Font("Segoe UI", 12F);
this.labelExportPath.Location = new System.Drawing.Point(23, 272);
this.labelExportPath.Name = "labelExportPath";
this.labelExportPath.Size = new System.Drawing.Size(280, 28);
this.labelExportPath.TabIndex = 13;
this.labelExportPath.Text = "Папка экспорта накладных";
//
// txtInvoiceExportPath
//
this.txtInvoiceExportPath.Font = new System.Drawing.Font("Segoe UI", 10F);
this.txtInvoiceExportPath.Location = new System.Drawing.Point(28, 304);
this.txtInvoiceExportPath.Name = "txtInvoiceExportPath";
this.txtInvoiceExportPath.Size = new System.Drawing.Size(320, 30);
this.txtInvoiceExportPath.TabIndex = 14;
//
// btnBrowseExportPath
//
this.btnBrowseExportPath.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold);
this.btnBrowseExportPath.Location = new System.Drawing.Point(354, 304);
this.btnBrowseExportPath.Name = "btnBrowseExportPath";
this.btnBrowseExportPath.Size = new System.Drawing.Size(49, 30);
this.btnBrowseExportPath.TabIndex = 15;
this.btnBrowseExportPath.Text = "...";
this.btnBrowseExportPath.Click += new System.EventHandler(this.btnBrowseExportPath_Click);
//
// btnConfirmRegistration // btnConfirmRegistration
// //
this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 244); this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 356);
this.btnConfirmRegistration.Name = "btnConfirmRegistration"; this.btnConfirmRegistration.Name = "btnConfirmRegistration";
this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43); this.btnConfirmRegistration.Size = new System.Drawing.Size(215, 43);
this.btnConfirmRegistration.TabIndex = 5; this.btnConfirmRegistration.TabIndex = 5;
@ -88,7 +139,7 @@
// //
// btnCloseThis // btnCloseThis
// //
this.btnCloseThis.Location = new System.Drawing.Point(28, 244); this.btnCloseThis.Location = new System.Drawing.Point(28, 356);
this.btnCloseThis.Name = "btnCloseThis"; this.btnCloseThis.Name = "btnCloseThis";
this.btnCloseThis.Size = new System.Drawing.Size(125, 43); this.btnCloseThis.Size = new System.Drawing.Size(125, 43);
this.btnCloseThis.TabIndex = 6; this.btnCloseThis.TabIndex = 6;
@ -100,7 +151,7 @@
this.checkTokenGained.AutoCheck = false; this.checkTokenGained.AutoCheck = false;
this.checkTokenGained.AutoSize = true; this.checkTokenGained.AutoSize = true;
this.checkTokenGained.Font = new System.Drawing.Font("Segoe UI", 12F); this.checkTokenGained.Font = new System.Drawing.Font("Segoe UI", 12F);
this.checkTokenGained.Location = new System.Drawing.Point(28, 304); this.checkTokenGained.Location = new System.Drawing.Point(28, 420);
this.checkTokenGained.Name = "checkTokenGained"; this.checkTokenGained.Name = "checkTokenGained";
this.checkTokenGained.Size = new System.Drawing.Size(171, 32); this.checkTokenGained.Size = new System.Drawing.Size(171, 32);
this.checkTokenGained.TabIndex = 7; this.checkTokenGained.TabIndex = 7;
@ -135,7 +186,12 @@
// //
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F); this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(436, 300); this.ClientSize = new System.Drawing.Size(436, 420);
this.Controls.Add(this.btnBrowseExportPath);
this.Controls.Add(this.txtInvoiceExportPath);
this.Controls.Add(this.labelExportPath);
this.Controls.Add(this.txtApiUrl);
this.Controls.Add(this.label5);
this.Controls.Add(this.panel1); this.Controls.Add(this.panel1);
this.Controls.Add(this.checkTokenGained); this.Controls.Add(this.checkTokenGained);
this.Controls.Add(this.btnCloseThis); this.Controls.Add(this.btnCloseThis);
@ -168,5 +224,10 @@
private System.Windows.Forms.CheckBox checkTokenGained; private System.Windows.Forms.CheckBox checkTokenGained;
private System.Windows.Forms.Panel panel1; private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Label label3; private System.Windows.Forms.Label label3;
private System.Windows.Forms.Label label5;
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
private System.Windows.Forms.Label labelExportPath;
private Elfisa.UI.Controls.ModernTextBox txtInvoiceExportPath;
private Elfisa.UI.Controls.ModernButton btnBrowseExportPath;
} }
} }

View File

@ -16,27 +16,55 @@ namespace Электронная_Фармация.HelpForms
Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors); Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
} }
string login = Settings.Default.stringLogin.ToString();
string password = Settings.Default.stringPassword.ToString();
string doWeHaveAToken = Settings.Default.stringToken.ToString();
private void btnCloseThis_Click(object sender, EventArgs e) private void btnCloseThis_Click(object sender, EventArgs e)
{ {
Close(); Close();
} }
private void btnBrowseExportPath_Click(object sender, EventArgs e)
{
using (var dialog = new FolderBrowserDialog())
{
dialog.Description = "Выберите папку для экспорта накладных";
dialog.ShowNewFolderButton = true;
if (!string.IsNullOrWhiteSpace(txtInvoiceExportPath.Text)
&& System.IO.Directory.Exists(txtInvoiceExportPath.Text.Trim()))
{
dialog.SelectedPath = txtInvoiceExportPath.Text.Trim();
}
if (dialog.ShowDialog(this) == DialogResult.OK)
{
txtInvoiceExportPath.Text = dialog.SelectedPath;
AppConfig.SetInvoiceExportPath(dialog.SelectedPath);
ToastNotification.ShowSuccess("Папка экспорта накладных сохранена");
}
}
}
private async void btnConfirmRegistration_Click(object sender, EventArgs e) private async void btnConfirmRegistration_Click(object sender, EventArgs e)
{ {
btnConfirmRegistration.Enabled = false; btnConfirmRegistration.Enabled = false;
btnCloseThis.Enabled = false; btnCloseThis.Enabled = false;
UseWaitCursor = true; UseWaitCursor = true;
string strLogin = (txtLogin.Text ?? string.Empty).Trim(); string strLogin = txtLogin.Text;
string strPassword = txtPassword.Text ?? string.Empty; string strPassword = txtPassword.Text;
if (strLogin.Length > 0 && strPassword.Length > 0) if (strLogin.Length > 0 && strPassword.Length > 0)
{ {
string apiUrl = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl) // Адрес API берём из поля и сохраняем ДО входа: иначе логин уходит
? AppConfig.DefaultApiBaseUrl // на старый сохранённый/дефолтный адрес, а не на введённый пользователем.
: Settings.Default.ApiBaseUrl; string apiUrl = txtApiUrl.Text.Trim();
Settings.Default.ApiBaseUrl = apiUrl;
AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
Settings.Default.Save();
LoginRequest loginRequest = new LoginRequest(); LoginRequest loginRequest = new LoginRequest();
loginRequest.Username = strLogin; loginRequest.Username = strLogin.Trim();
loginRequest.Password = strPassword.Trim(); loginRequest.Password = strPassword.Trim();
var client = new ApiClient(AppConfig.NormalizeApiBaseUrl(apiUrl)); var client = new ApiClient(AppConfig.NormalizeApiBaseUrl(apiUrl));
@ -46,16 +74,25 @@ namespace Электронная_Фармация.HelpForms
{ {
string token = await client.LoginAsync(loginRequest.Username, loginRequest.Password); string token = await client.LoginAsync(loginRequest.Username, loginRequest.Password);
// Сохраняем только логин и токен; пароль в настройках не храним.
Settings.Default.stringLogin = strLogin; Settings.Default.stringLogin = strLogin;
Settings.Default.stringPassword = string.Empty; Settings.Default.stringPassword = strPassword;
Settings.Default.stringToken = token; Settings.Default.stringToken = token;
Settings.Default.ApiBaseUrl = client.BaseUrl; Settings.Default.ApiBaseUrl = client.BaseUrl;
AppConfig.SetInvoiceExportPath(txtInvoiceExportPath.Text);
Settings.Default.Save(); Settings.Default.Save();
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}"); AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
ToastNotification.ShowSuccess("Авторизация успешно выполнена"); ToastNotification.ShowSuccess("Авторизация успешно выполнена");
// После входа обязательно запрашиваем Location ID для отправки заказов.
if (!HF_LocationId.PromptAndSave(this))
{
ToastNotification.ShowCustom(
"Location ID не указан. Отправка заказов будет недоступна, пока его не заполните.",
System.Drawing.Color.DarkOrange,
System.Drawing.Color.White);
}
DialogResult = DialogResult.OK; DialogResult = DialogResult.OK;
Close(); Close();
return; return;
@ -85,10 +122,14 @@ namespace Электронная_Фармация.HelpForms
{ {
UiThemeHelper.ApplyToControlTree(this); UiThemeHelper.ApplyToControlTree(this);
WindowChromeHelper.ApplyDialogChrome(this, panel1, label3); WindowChromeHelper.ApplyDialogChrome(this, panel1, label3);
// Только последний успешный логин; пароль пользователь вводит каждый раз. txtLogin.Text = login;
txtLogin.Text = Settings.Default.stringLogin ?? string.Empty; txtPassword.Text = password;
txtPassword.Text = string.Empty; txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
checkTokenGained.Checked = !string.IsNullOrWhiteSpace(Settings.Default.stringToken); ? AppConfig.DefaultApiBaseUrl
: Settings.Default.ApiBaseUrl;
txtInvoiceExportPath.Text = AppConfig.InvoiceExportPath;
checkTokenGained.Checked = doWeHaveAToken.Length > 0;
} }
} }
} }

View File

@ -1,192 +0,0 @@
namespace Электроннаяармация.HelpForms
{
partial class HF_Settings
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
this.panelHeader = new System.Windows.Forms.Panel();
this.lblTitle = new System.Windows.Forms.Label();
this.lblHint = new System.Windows.Forms.Label();
this.lblLocationId = new System.Windows.Forms.Label();
this.txtLocationId = new Elfisa.UI.Controls.ModernTextBox();
this.lblApiUrl = new System.Windows.Forms.Label();
this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox();
this.lblInvoiceExportPath = new System.Windows.Forms.Label();
this.txtInvoiceExportPath = new Elfisa.UI.Controls.ModernTextBox();
this.btnBrowseExportPath = new Elfisa.UI.Controls.ModernButton();
this.btnCancel = new Elfisa.UI.Controls.ModernButton();
this.btnSave = new Elfisa.UI.Controls.ModernButton();
this.panelHeader.SuspendLayout();
this.SuspendLayout();
//
// panelHeader
//
this.panelHeader.Controls.Add(this.lblTitle);
this.panelHeader.Dock = System.Windows.Forms.DockStyle.Top;
this.panelHeader.Location = new System.Drawing.Point(0, 0);
this.panelHeader.Name = "panelHeader";
this.panelHeader.Size = new System.Drawing.Size(560, 56);
this.panelHeader.TabIndex = 0;
//
// lblTitle
//
this.lblTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 13.8F, System.Drawing.FontStyle.Bold);
this.lblTitle.ForeColor = System.Drawing.Color.White;
this.lblTitle.Location = new System.Drawing.Point(12, 10);
this.lblTitle.Name = "lblTitle";
this.lblTitle.Size = new System.Drawing.Size(520, 36);
this.lblTitle.TabIndex = 0;
this.lblTitle.Text = "Настройки";
this.lblTitle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// lblHint
//
this.lblHint.Font = new System.Drawing.Font("Segoe UI", 10F);
this.lblHint.Location = new System.Drawing.Point(24, 72);
this.lblHint.Name = "lblHint";
this.lblHint.Size = new System.Drawing.Size(512, 46);
this.lblHint.TabIndex = 1;
this.lblHint.Text = "Location ID ниже — запасной. Для каждой аптеки задайте свой ID в «Грузополучатели». Также укажите API и папку накладных.";
//
// lblLocationId
//
this.lblLocationId.AutoSize = true;
this.lblLocationId.Font = new System.Drawing.Font("Segoe UI", 11F);
this.lblLocationId.Location = new System.Drawing.Point(24, 124);
this.lblLocationId.Name = "lblLocationId";
this.lblLocationId.Size = new System.Drawing.Size(100, 25);
this.lblLocationId.TabIndex = 2;
this.lblLocationId.Text = "Location ID";
//
// txtLocationId
//
this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 11F);
this.txtLocationId.Location = new System.Drawing.Point(28, 152);
this.txtLocationId.Name = "txtLocationId";
this.txtLocationId.Size = new System.Drawing.Size(508, 32);
this.txtLocationId.TabIndex = 3;
//
// lblApiUrl
//
this.lblApiUrl.AutoSize = true;
this.lblApiUrl.Font = new System.Drawing.Font("Segoe UI", 11F);
this.lblApiUrl.Location = new System.Drawing.Point(24, 196);
this.lblApiUrl.Name = "lblApiUrl";
this.lblApiUrl.Size = new System.Drawing.Size(127, 25);
this.lblApiUrl.TabIndex = 4;
this.lblApiUrl.Text = "Адрес сервера";
//
// txtApiUrl
//
this.txtApiUrl.Font = new System.Drawing.Font("Segoe UI", 11F);
this.txtApiUrl.Location = new System.Drawing.Point(28, 224);
this.txtApiUrl.Name = "txtApiUrl";
this.txtApiUrl.Size = new System.Drawing.Size(508, 32);
this.txtApiUrl.TabIndex = 5;
//
// lblInvoiceExportPath
//
this.lblInvoiceExportPath.AutoSize = true;
this.lblInvoiceExportPath.Font = new System.Drawing.Font("Segoe UI", 11F);
this.lblInvoiceExportPath.Location = new System.Drawing.Point(24, 268);
this.lblInvoiceExportPath.Name = "lblInvoiceExportPath";
this.lblInvoiceExportPath.Size = new System.Drawing.Size(228, 25);
this.lblInvoiceExportPath.TabIndex = 6;
this.lblInvoiceExportPath.Text = "Папка экспорта накладных";
//
// txtInvoiceExportPath
//
this.txtInvoiceExportPath.Font = new System.Drawing.Font("Segoe UI", 11F);
this.txtInvoiceExportPath.Location = new System.Drawing.Point(28, 296);
this.txtInvoiceExportPath.Name = "txtInvoiceExportPath";
this.txtInvoiceExportPath.Size = new System.Drawing.Size(448, 32);
this.txtInvoiceExportPath.TabIndex = 7;
//
// btnBrowseExportPath
//
this.btnBrowseExportPath.Font = new System.Drawing.Font("Segoe UI Semibold", 10F, System.Drawing.FontStyle.Bold);
this.btnBrowseExportPath.Location = new System.Drawing.Point(487, 296);
this.btnBrowseExportPath.Name = "btnBrowseExportPath";
this.btnBrowseExportPath.Size = new System.Drawing.Size(49, 32);
this.btnBrowseExportPath.TabIndex = 8;
this.btnBrowseExportPath.Text = "...";
this.btnBrowseExportPath.Click += new System.EventHandler(this.btnBrowseExportPath_Click);
//
// btnCancel
//
this.btnCancel.Location = new System.Drawing.Point(28, 356);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(140, 40);
this.btnCancel.TabIndex = 9;
this.btnCancel.Text = "Отмена";
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
//
// btnSave
//
this.btnSave.Location = new System.Drawing.Point(364, 356);
this.btnSave.Name = "btnSave";
this.btnSave.Size = new System.Drawing.Size(172, 40);
this.btnSave.TabIndex = 10;
this.btnSave.Text = "Сохранить";
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
//
// HF_Settings
//
this.AcceptButton = this.btnSave;
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.btnCancel;
this.ClientSize = new System.Drawing.Size(560, 420);
this.Controls.Add(this.btnSave);
this.Controls.Add(this.btnCancel);
this.Controls.Add(this.btnBrowseExportPath);
this.Controls.Add(this.txtInvoiceExportPath);
this.Controls.Add(this.lblInvoiceExportPath);
this.Controls.Add(this.txtApiUrl);
this.Controls.Add(this.lblApiUrl);
this.Controls.Add(this.txtLocationId);
this.Controls.Add(this.lblLocationId);
this.Controls.Add(this.lblHint);
this.Controls.Add(this.panelHeader);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedToolWindow;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "HF_Settings";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Load += new System.EventHandler(this.HF_Settings_Load);
this.panelHeader.ResumeLayout(false);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Panel panelHeader;
private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblHint;
private System.Windows.Forms.Label lblLocationId;
private Elfisa.UI.Controls.ModernTextBox txtLocationId;
private System.Windows.Forms.Label lblApiUrl;
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
private System.Windows.Forms.Label lblInvoiceExportPath;
private Elfisa.UI.Controls.ModernTextBox txtInvoiceExportPath;
private Elfisa.UI.Controls.ModernButton btnBrowseExportPath;
private Elfisa.UI.Controls.ModernButton btnCancel;
private Elfisa.UI.Controls.ModernButton btnSave;
}
}

View File

@ -1,95 +0,0 @@
using System;
using System.Windows.Forms;
using Elfisa.UI.Helpers;
using Elfisa.UI.Theming;
using Электроннаяармация.Classes;
using Электроннаяармация.Properties;
namespace Электроннаяармация.HelpForms
{
public partial class HF_Settings : Form
{
public HF_Settings()
{
InitializeComponent();
Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
}
private void HF_Settings_Load(object sender, EventArgs e)
{
UiThemeHelper.ApplyToControlTree(this);
WindowChromeHelper.ApplyDialogChrome(this, panelHeader, lblTitle);
txtLocationId.Text = AppConfig.LocationId;
txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
? AppConfig.DefaultApiBaseUrl
: Settings.Default.ApiBaseUrl;
txtInvoiceExportPath.Text = AppConfig.InvoiceExportPath;
}
private void btnBrowseExportPath_Click(object sender, EventArgs e)
{
using (var dialog = new FolderBrowserDialog())
{
dialog.Description = "Выберите папку для экспорта накладных";
dialog.ShowNewFolderButton = true;
if (!string.IsNullOrWhiteSpace(txtInvoiceExportPath.Text) &&
System.IO.Directory.Exists(txtInvoiceExportPath.Text.Trim()))
{
dialog.SelectedPath = txtInvoiceExportPath.Text.Trim();
}
if (dialog.ShowDialog(this) == DialogResult.OK)
{
txtInvoiceExportPath.Text = dialog.SelectedPath;
}
}
}
private void btnSave_Click(object sender, EventArgs e)
{
var locationId = (txtLocationId.Text ?? string.Empty).Trim();
var apiUrl = AppConfig.NormalizeApiBaseUrl(txtApiUrl.Text.Trim());
var invoiceExportPath = (txtInvoiceExportPath.Text ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(locationId))
{
UiDialogs.ShowError("Укажите Location ID торговой точки.", "Настройки", this);
txtLocationId.Focus();
return;
}
if (string.IsNullOrWhiteSpace(apiUrl))
{
UiDialogs.ShowError("Укажите адрес API сервера.", "Настройки", this);
txtApiUrl.Focus();
return;
}
if (!string.IsNullOrWhiteSpace(invoiceExportPath) &&
!System.IO.Directory.Exists(invoiceExportPath))
{
UiDialogs.ShowError("Указанная папка для накладных не существует.", "Настройки", this);
txtInvoiceExportPath.Focus();
return;
}
AppConfig.SetLocationId(locationId);
Settings.Default.ApiBaseUrl = apiUrl;
AppConfig.SetInvoiceExportPath(invoiceExportPath);
Settings.Default.Save();
AppDebugLog.Info("Settings", $"Настройки сохранены. ApiBaseUrl={apiUrl}, LocationId={locationId}, InvoiceExportPath={invoiceExportPath}");
ToastNotification.ShowSuccess("Настройки сохранены");
DialogResult = DialogResult.OK;
Close();
}
private void btnCancel_Click(object sender, EventArgs e)
{
DialogResult = DialogResult.Cancel;
Close();
}
}
}

View File

@ -1,107 +0,0 @@
namespace Электроннаяармация.HelpForms
{
partial class HF_UpdateAvailable
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
private void InitializeComponent()
{
this.panelHeader = new System.Windows.Forms.Panel();
this.lblTitle = new System.Windows.Forms.Label();
this.lblMessage = new System.Windows.Forms.Label();
this.btnDownload = new Elfisa.UI.Controls.ModernButton();
this.btnExit = new Elfisa.UI.Controls.ModernButton();
this.panelHeader.SuspendLayout();
this.SuspendLayout();
//
// panelHeader
//
this.panelHeader.Controls.Add(this.lblTitle);
this.panelHeader.Dock = System.Windows.Forms.DockStyle.Top;
this.panelHeader.Location = new System.Drawing.Point(0, 0);
this.panelHeader.Name = "panelHeader";
this.panelHeader.Size = new System.Drawing.Size(520, 56);
this.panelHeader.TabIndex = 0;
//
// lblTitle
//
this.lblTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 13.8F, System.Drawing.FontStyle.Bold);
this.lblTitle.ForeColor = System.Drawing.Color.White;
this.lblTitle.Location = new System.Drawing.Point(12, 10);
this.lblTitle.Name = "lblTitle";
this.lblTitle.Size = new System.Drawing.Size(480, 36);
this.lblTitle.TabIndex = 0;
this.lblTitle.Text = "Доступно новое обновление";
this.lblTitle.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
//
// lblMessage
//
this.lblMessage.Font = new System.Drawing.Font("Segoe UI", 11F);
this.lblMessage.Location = new System.Drawing.Point(24, 72);
this.lblMessage.Name = "lblMessage";
this.lblMessage.Size = new System.Drawing.Size(472, 120);
this.lblMessage.TabIndex = 1;
this.lblMessage.Text = "Доступно новое обновление";
//
// btnDownload
//
this.btnDownload.Font = new System.Drawing.Font("Segoe UI", 11F);
this.btnDownload.Location = new System.Drawing.Point(24, 220);
this.btnDownload.Name = "btnDownload";
this.btnDownload.Size = new System.Drawing.Size(180, 40);
this.btnDownload.TabIndex = 2;
this.btnDownload.Text = "Скачать";
this.btnDownload.Click += new System.EventHandler(this.btnDownload_Click);
//
// btnExit
//
this.btnExit.Font = new System.Drawing.Font("Segoe UI", 11F);
this.btnExit.Location = new System.Drawing.Point(316, 220);
this.btnExit.Name = "btnExit";
this.btnExit.Size = new System.Drawing.Size(180, 40);
this.btnExit.TabIndex = 3;
this.btnExit.Text = "Выход";
this.btnExit.Click += new System.EventHandler(this.btnExit_Click);
//
// HF_UpdateAvailable
//
this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(520, 290);
this.Controls.Add(this.btnExit);
this.Controls.Add(this.btnDownload);
this.Controls.Add(this.lblMessage);
this.Controls.Add(this.panelHeader);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "HF_UpdateAvailable";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Обновление";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.HF_UpdateAvailable_FormClosing);
this.panelHeader.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Panel panelHeader;
private System.Windows.Forms.Label lblTitle;
private System.Windows.Forms.Label lblMessage;
private Elfisa.UI.Controls.ModernButton btnDownload;
private Elfisa.UI.Controls.ModernButton btnExit;
}
}

View File

@ -1,104 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using Электроннаяармация.Classes;
namespace Электроннаяармация.HelpForms
{
public partial class HF_UpdateAvailable : Form
{
private readonly UpdateCheckResult _result;
private bool _updating;
public HF_UpdateAvailable(UpdateCheckResult result)
{
_result = result ?? throw new ArgumentNullException(nameof(result));
InitializeComponent();
UiThemeHelper.ApplyToControlTree(this);
var latest = string.IsNullOrWhiteSpace(_result.LatestVersion) ? "?" : _result.LatestVersion;
var local = string.IsNullOrWhiteSpace(_result.LocalVersion) ? "?" : _result.LocalVersion;
lblMessage.Text =
$"Доступно новое обновление {latest} (у вас {local}).{Environment.NewLine}{Environment.NewLine}" +
"Нажмите «Обновить» — программа скачает и установит новую версию сама." + Environment.NewLine +
"Локальная база и данные сохранятся.";
btnDownload.Text = "Обновить";
}
private async void btnDownload_Click(object sender, EventArgs e)
{
if (_updating)
{
return;
}
if (string.IsNullOrWhiteSpace(_result.DownloadUrl) &&
string.IsNullOrWhiteSpace(_result.ReleaseUrl))
{
UiDialogs.ShowError("Ссылка на обновление не задана.", "Обновление", this);
return;
}
_updating = true;
btnDownload.Enabled = false;
btnExit.Enabled = false;
ControlBox = false;
var progress = new Progress<string>(msg =>
{
lblMessage.Text = msg;
});
try
{
await AutoUpdater.ApplyUpdateAsync(_result, progress);
lblMessage.Text = "Обновление скачано. Перезапуск...";
// Снимаем блокировку закрытия, иначе диалог зависает навечно
// на «Перезапуск...» и updater ждёт PID, который не завершается.
_updating = false;
DialogResult = DialogResult.OK;
Close();
}
catch (Exception ex)
{
AppDebugLog.Error("Update", "Автообновление не удалось", ex);
_updating = false;
btnDownload.Enabled = true;
btnExit.Enabled = true;
ControlBox = true;
UiDialogs.ShowError(
"Не удалось установить обновление автоматически." + Environment.NewLine +
ex.Message,
"Обновление",
this);
}
}
private void btnExit_Click(object sender, EventArgs e)
{
if (_updating)
{
return;
}
DialogResult = DialogResult.Abort;
Close();
}
private void HF_UpdateAvailable_FormClosing(object sender, FormClosingEventArgs e)
{
// Блокируем только ручное закрытие во время скачивания.
if (_updating && DialogResult != DialogResult.OK)
{
e.Cancel = true;
return;
}
// Крестик / Alt+F4 — выход из приложения (не пускать в работу).
if (DialogResult != DialogResult.Abort && DialogResult != DialogResult.OK)
{
DialogResult = DialogResult.Abort;
}
}
}
}

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
using System.Net; using System.Net;
using Электроннаяармация.Classes; using Электроннаяармация.Classes;
@ -12,7 +12,6 @@ namespace Электронная_Фармация
{ {
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
AppConfig.MigrateLegacySettings(); AppConfig.MigrateLegacySettings();
AppConfig.UpgradeUserSettingsIfNeeded();
AppDebugLog.Info("App", $"Запуск. ApiBaseUrl={AppConfig.ApiBaseUrl}, Log={AppDebugLog.CurrentLogFilePath}"); AppDebugLog.Info("App", $"Запуск. ApiBaseUrl={AppConfig.ApiBaseUrl}, Log={AppDebugLog.CurrentLogFilePath}");
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);

View File

@ -2,36 +2,35 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ // Общие сведения об этой сборке предоставляются следующим набором
// пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ. пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ, // набора атрибутов. Измените значения этих атрибутов для изменения сведений,
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ. // связанных со сборкой.
[assembly: AssemblyTitle("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")] [assembly: AssemblyTitle("Электронная Фармация")]
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")] [assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")] [assembly: AssemblyProduct("Электронная Фармация")]
[assembly: AssemblyCopyright("Copyright пїЅ 2023")] [assembly: AssemblyCopyright("Copyright © 2023")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ False пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ // Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми
// пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ COM. пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ // для компонентов COM. Если необходимо обратиться к типу в этой сборке через
// COM, пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅ TRUE пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ. // COM, следует установить атрибут ComVisible в TRUE для этого типа.
[assembly: ComVisible(false)] [assembly: ComVisible(false)]
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ GUID пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ, пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ COM // Следующий GUID служит для идентификации библиотеки типов, если этот проект будет видимым для COM
[assembly: Guid("145d8060-4187-4871-b4fb-8df795727b66")] [assembly: Guid("145d8060-4187-4871-b4fb-8df795727b66")]
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ: // Сведения о версии сборки состоят из указанных ниже четырех значений:
// //
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ // Основной номер версии
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ // Дополнительный номер версии
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ // Номер сборки
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ // Редакция
// //
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ // Можно задать все значения или принять номера сборки и редакции по умолчанию
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ: // используя "*", как показано ниже:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.29.0")] [assembly: AssemblyVersion("1.0.3.0")]
[assembly: AssemblyFileVersion("1.0.29.0")] [assembly: AssemblyFileVersion("1.0.3.0")]

View File

@ -1,4 +1,4 @@
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// <auto-generated> // <auto-generated>
// Этот код создан программой. // Этот код создан программой.
// Исполняемая версия:4.0.30319.42000 // Исполняемая версия:4.0.30319.42000
@ -131,18 +131,6 @@ namespace Электронная_Фармация.Properties {
} }
} }
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
public int PriceListSplitterDistance {
get {
return ((int)(this["PriceListSplitterDistance"]));
}
set {
this["PriceListSplitterDistance"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()] [global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")] [global::System.Configuration.DefaultSettingValueAttribute("")]
@ -155,77 +143,5 @@ namespace Электронная_Фармация.Properties {
} }
} }
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("01.01.0001 00:00:00")]
public global::System.DateTime LastPriceUpdateUtc {
get {
return ((global::System.DateTime)(this["LastPriceUpdateUtc"]));
}
set {
this["LastPriceUpdateUtc"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("01.01.0001 00:00:00")]
public global::System.DateTime LastPriceReminderUtc {
get {
return ((global::System.DateTime)(this["LastPriceReminderUtc"]));
}
set {
this["LastPriceReminderUtc"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("https://git.24pharmdata.ru")]
public string UpdateCheckBaseUrl {
get {
return ((string)(this["UpdateCheckBaseUrl"]));
}
set {
this["UpdateCheckBaseUrl"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("pharmdata/elfisa-pharmacy")]
public string UpdateRepo {
get {
return ((string)(this["UpdateRepo"]));
}
set {
this["UpdateRepo"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string UpdateApiToken {
get {
return ((string)(this["UpdateApiToken"]));
}
set {
this["UpdateApiToken"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool UpgradeSettings {
get {
return ((bool)(this["UpgradeSettings"]));
}
set {
this["UpgradeSettings"] = value;
}
}
} }
} }

View File

@ -1,4 +1,4 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="Электронная_Фармация.Properties" GeneratedClassName="Settings"> <SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="Электронная_Фармация.Properties" GeneratedClassName="Settings">
<Profiles /> <Profiles />
<Settings> <Settings>
@ -29,29 +29,8 @@
<Setting Name="UseDarkTheme" Type="System.Boolean" Scope="User"> <Setting Name="UseDarkTheme" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value> <Value Profile="(Default)">False</Value>
</Setting> </Setting>
<Setting Name="PriceListSplitterDistance" Type="System.Int32" Scope="User">
<Value Profile="(Default)">0</Value>
</Setting>
<Setting Name="InvoiceExportPath" Type="System.String" Scope="User"> <Setting Name="InvoiceExportPath" Type="System.String" Scope="User">
<Value Profile="(Default)" /> <Value Profile="(Default)" />
</Setting> </Setting>
<Setting Name="LastPriceUpdateUtc" Type="System.DateTime" Scope="User">
<Value Profile="(Default)">01.01.0001 00:00:00</Value>
</Setting>
<Setting Name="LastPriceReminderUtc" Type="System.DateTime" Scope="User">
<Value Profile="(Default)">01.01.0001 00:00:00</Value>
</Setting>
<Setting Name="UpdateCheckBaseUrl" Type="System.String" Scope="User">
<Value Profile="(Default)">https://git.24pharmdata.ru</Value>
</Setting>
<Setting Name="UpdateRepo" Type="System.String" Scope="User">
<Value Profile="(Default)">pharmdata/elfisa-pharmacy</Value>
</Setting>
<Setting Name="UpdateApiToken" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="UpgradeSettings" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
</Settings> </Settings>
</SettingsFile> </SettingsFile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -37,7 +37,7 @@ namespace Электронная_Фармация.UserControls
dtpDateFrom.Value = DateTime.Now.AddDays(-3); dtpDateFrom.Value = DateTime.Now.AddDays(-3);
dtpDateTo.Value = DateTime.Now; dtpDateTo.Value = DateTime.Now;
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), string.Empty); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), string.Empty);
loadSuppliers(); loadSuppliers();
comboSuppliers.Visible = true; comboSuppliers.Visible = true;
@ -334,8 +334,8 @@ namespace Электронная_Фармация.UserControls
var addon = parts.Count == 0 ? string.Empty : " and " + string.Join(" and ", parts); var addon = parts.Count == 0 ? string.Empty : " and " + string.Join(" and ", parts);
LoadOrders( LoadOrders(
dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateFrom.Value.ToString("dd.MM.yyyy"),
dtpDateTo.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("dd.MM.yyyy"),
addon); addon);
} }
@ -394,9 +394,9 @@ namespace Электронная_Фармация.UserControls
try try
{ {
var service = new InvoiceSyncService(); var service = new InvoiceRequestService();
service.SyncFromServerAsync().GetAwaiter().GetResult(); service.CreateOrUpdateFromOrder(orderId);
ToastNotification.ShowSuccess("Накладные обновлены с сервера."); ToastNotification.ShowSuccess("Накладная добавлена в раздел \"Накладные\".");
var shell = FindForm() as ElectroPharmacy; var shell = FindForm() as ElectroPharmacy;
shell?.OpenInvoicesTab(orderId); shell?.OpenInvoicesTab(orderId);
@ -411,14 +411,6 @@ namespace Электронная_Фармация.UserControls
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
// OrderDate в БД исторически dd.MM.yyyy — сравниваем как даты, не как строки,
// иначе «03.08» не попадает в диапазон «с 31.07» (лексикографически).
const string orderDateIso = @"CASE
WHEN length(OrderDate)=10 AND substr(OrderDate,3,1)='.' THEN
substr(OrderDate,7,4)||'-'||substr(OrderDate,4,2)||'-'||substr(OrderDate,1,2)
ELSE OrderDate
END";
string qShowMeOrders = $@" string qShowMeOrders = $@"
select select
[Id_Order], [Id_Order],
@ -433,8 +425,8 @@ select
[ConsigneeName] [ConsigneeName]
from Orders from Orders
where where
({orderDateIso}) >= '{dateFrom}' OrderDate >= '{dateFrom}'
and ({orderDateIso}) <= '{dateTo}' and OrderDate <= '{dateTo}'
{filters} {filters}
"; ";
@ -516,11 +508,7 @@ and ({orderDateIso}) <= '{dateTo}'
void loadConsignees() void loadConsignees()
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string qShowMeConsigneesList = @" string qShowMeConsigneesList = "select ConsigneesName from Consignees order by [ConsigneesUseAsDefault] desc";
select distinct ConsigneeName
from Orders
where trim(ifnull(ConsigneeName, '')) <> ''
order by ConsigneeName";
using (SQLiteConnection conConsignees = new SQLiteConnection(connectionStringToLocalDB)) using (SQLiteConnection conConsignees = new SQLiteConnection(connectionStringToLocalDB))
{ {
@ -539,7 +527,7 @@ order by ConsigneeName";
var items = new List<string> { "Грузополучатель" }; var items = new List<string> { "Грузополучатель" };
items.AddRange(tableConsignees.Rows.Cast<DataRow>() items.AddRange(tableConsignees.Rows.Cast<DataRow>()
.Select(r => r["ConsigneeName"]?.ToString()) .Select(r => r["ConsigneesName"]?.ToString())
.Where(n => !string.IsNullOrWhiteSpace(n))); .Where(n => !string.IsNullOrWhiteSpace(n)));
_comboConsignees.DataSource = items; _comboConsignees.DataSource = items;
_comboConsignees.SelectedIndex = 0; _comboConsignees.SelectedIndex = 0;
@ -559,11 +547,7 @@ order by ConsigneeName";
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string qShowMeSuppliersList = @" string qShowMeSuppliersList = "select SuppliersName from suppliers order by [SuppliersName] asc";
select distinct SupplierName
from Orders
where trim(ifnull(SupplierName, '')) <> ''
order by SupplierName asc";
using (SQLiteConnection conSuppliers = new SQLiteConnection(connectionStringToLocalDB)) using (SQLiteConnection conSuppliers = new SQLiteConnection(connectionStringToLocalDB))
{ {
@ -575,7 +559,7 @@ order by SupplierName asc";
DataTable tableSuppliers = new DataTable(); DataTable tableSuppliers = new DataTable();
daSuppliersList.Fill(tableSuppliers); daSuppliersList.Fill(tableSuppliers);
comboSuppliers.DataSource = tableSuppliers; comboSuppliers.DataSource = tableSuppliers;
comboSuppliers.DisplayMember = "SupplierName"; comboSuppliers.DisplayMember = "SuppliersName";
comboSuppliers.Text = "Поставщик"; comboSuppliers.Text = "Поставщик";
} }
catch (Exception ex) catch (Exception ex)
@ -692,25 +676,25 @@ order by SupplierName asc";
{ {
txtGoodFromOrders.Text = "Товар"; txtGoodFromOrders.Text = "Товар";
filter = string.Empty; filter = string.Empty;
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
private void dtpDateFrom_ValueChanged(object sender, EventArgs e) private void dtpDateFrom_ValueChanged(object sender, EventArgs e)
{ {
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
private void dtpDateTo_ValueChanged(object sender, EventArgs e) private void dtpDateTo_ValueChanged(object sender, EventArgs e)
{ {
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
private void dtpDateTo_KeyPress(object sender, KeyPressEventArgs e) private void dtpDateTo_KeyPress(object sender, KeyPressEventArgs e)
{ {
if (e.KeyChar == (Char)Keys.Enter) if (e.KeyChar == (Char)Keys.Enter)
{ {
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
} }
@ -718,7 +702,7 @@ order by SupplierName asc";
{ {
if (e.KeyChar == (Char)Keys.Enter) if (e.KeyChar == (Char)Keys.Enter)
{ {
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
} }
@ -742,7 +726,7 @@ order by SupplierName asc";
and Id_Order in (select Id_Order from OrderItems where DrugName like '%{txtGoodFromOrders.Text}%') and Id_Order in (select Id_Order from OrderItems where DrugName like '%{txtGoodFromOrders.Text}%')
"; ";
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
} }
} }
@ -889,7 +873,7 @@ where [id_order] = '{idPriceList}'
and OrderNumber like '%{txtOrderNumberFilter.Text}%' and OrderNumber like '%{txtOrderNumberFilter.Text}%'
"; ";
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
} }
} }
@ -914,7 +898,7 @@ and OrderNumber like '%{txtOrderNumberFilter.Text}%'
{ {
txtOrderNumberFilter.Text = "Номер заказа"; txtOrderNumberFilter.Text = "Номер заказа";
filter = string.Empty; filter = string.Empty;
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
} }
private void btnAddComment_Click(object sender, EventArgs e) private void btnAddComment_Click(object sender, EventArgs e)
@ -927,7 +911,7 @@ and OrderNumber like '%{txtOrderNumberFilter.Text}%'
HF_Comment hf_comment = new HF_Comment(OrderNumber, Id_Order); HF_Comment hf_comment = new HF_Comment(OrderNumber, Id_Order);
hf_comment.ShowDialog(); hf_comment.ShowDialog();
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), string.Empty); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), string.Empty);
} }
} }
@ -964,7 +948,7 @@ and OrderNumber like '%{txtOrderNumberFilter.Text}%'
//ToastNotification.ShowCustom($"Заказ {OrderNumber} был удалён", Color.DarkOrange, Color.White); //ToastNotification.ShowCustom($"Заказ {OrderNumber} был удалён", Color.DarkOrange, Color.White);
ToastNotification.ShowSuccess($"Заказ {OrderNumber} был удалён"); ToastNotification.ShowSuccess($"Заказ {OrderNumber} был удалён");
//MessageBox.Show($"Заказ {OrderNumber} был успешно удалён.", "Удаление заказа", MessageBoxButtons.OK, MessageBoxIcon.Information); //MessageBox.Show($"Заказ {OrderNumber} был успешно удалён.", "Удаление заказа", MessageBoxButtons.OK, MessageBoxIcon.Information);
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), string.Empty); LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), string.Empty);
} }
catch(Exception ex) catch(Exception ex)
{ {

View File

@ -57,11 +57,7 @@ namespace Электронная_Фармация.UserControls
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string qShowMeSuppliersList = @" string qShowMeSuppliersList = "select SuppliersName from suppliers order by [SuppliersName] asc";
select distinct SupplierName
from Invoice
where trim(ifnull(SupplierName, '')) <> ''
order by SupplierName asc";
using (SQLiteConnection conSuppliers = new SQLiteConnection(connectionStringToLocalDB)) using (SQLiteConnection conSuppliers = new SQLiteConnection(connectionStringToLocalDB))
{ {
@ -73,7 +69,7 @@ order by SupplierName asc";
DataTable tableSuppliers = new DataTable(); DataTable tableSuppliers = new DataTable();
daSuppliersList.Fill(tableSuppliers); daSuppliersList.Fill(tableSuppliers);
comboSuppliers.DataSource = tableSuppliers; comboSuppliers.DataSource = tableSuppliers;
comboSuppliers.DisplayMember = "SupplierName"; comboSuppliers.DisplayMember = "SuppliersName";
comboSuppliers.Text = "Поставщик"; comboSuppliers.Text = "Поставщик";
} }
catch(Exception ex) catch(Exception ex)
@ -91,11 +87,7 @@ order by SupplierName asc";
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string qShowMeConsigneesList = @" string qShowMeConsigneesList = "select ConsigneesName from Consignees order by [ConsigneesUseAsDefault] desc";
select distinct ConsigneesName
from Invoice
where trim(ifnull(ConsigneesName, '')) <> ''
order by ConsigneesName asc";
using (SQLiteConnection conConsignees = new SQLiteConnection(connectionStringToLocalDB)) using (SQLiteConnection conConsignees = new SQLiteConnection(connectionStringToLocalDB))
{ {
@ -135,8 +127,8 @@ select
i.[idInvoice] as [ИД накладной], i.[idInvoice] as [ИД накладной],
i.[InvoiceNumber] as [Номер], i.[InvoiceNumber] as [Номер],
i.[InvoiceDate] as [Дата], i.[InvoiceDate] as [Дата],
ifnull(i.[SupplierName], '') as [Поставщик], coalesce(nullif(i.[SupplierName], ''), (select s.[SuppliersName] from [Suppliers] s where s.[codeSuppliers] = i.[CodeSupplier])) as [Поставщик],
ifnull(i.[ConsigneesName], '') as [Грузополучатель], coalesce(nullif(i.[ConsigneesName], ''), (select c.[ConsigneesName] from [Consignees] c where c.[codeConsignees] = i.[CodeConsignees])) as [Грузополучатель],
i.[SourceOrderNumber] as [Номер заказа], i.[SourceOrderNumber] as [Номер заказа],
coalesce(i.[InvoiceSum], i.[SumWithNDS], i.[SumWithoutNDS], 0) as [Сумма], coalesce(i.[InvoiceSum], i.[SumWithNDS], i.[SumWithoutNDS], 0) as [Сумма],
coalesce(i.[RefuseSum], 0) as [Сумма отказа] coalesce(i.[RefuseSum], 0) as [Сумма отказа]
@ -150,12 +142,12 @@ where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик") if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик")
{ {
sql += " and ifnull(i.[SupplierName], '') like @supplierName"; sql += " and coalesce(nullif(i.[SupplierName], ''), (select s.[SuppliersName] from [Suppliers] s where s.[codeSuppliers] = i.[CodeSupplier])) like @supplierName";
} }
if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель") if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель")
{ {
sql += " and ifnull(i.[ConsigneesName], '') like @consigneeName"; sql += " and coalesce(nullif(i.[ConsigneesName], ''), (select c.[ConsigneesName] from [Consignees] c where c.[codeConsignees] = i.[CodeConsignees])) like @consigneeName";
} }
if (!string.IsNullOrWhiteSpace(_initialSourceOrderId)) if (!string.IsNullOrWhiteSpace(_initialSourceOrderId))

View File

@ -11,7 +11,6 @@ using System.Data.SQLite;
using Электроннаяармация.UserControls; using Электроннаяармация.UserControls;
using System.Data.Common; using System.Data.Common;
using Электроннаяармация.Classes; using Электроннаяармация.Classes;
using Электроннаяармация.Properties;
using Elfisa.UI.Theming; using Elfisa.UI.Theming;
namespace Электроннаяармация.UserControls namespace Электроннаяармация.UserControls
@ -40,24 +39,7 @@ namespace Электронная_Фармация.UserControls
public void loadDataAboutOrderV2(string Supplier) public void loadDataAboutOrderV2(string Supplier)
{ {
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''"); string qShowMeOrderItems = "select * from TempOrderItems";
string qShowMeOrderItems = $@"
SELECT
[guid_es],
[es_code],
[supplier_id],
[DrugName],
[SupplierName],
[Price],
[Quantity],
[ExpiryPeriod],
[Description],
[Zakaz],
[SummaZakaza],
[id_PriceList_Item],
[supplier_price_id]
FROM [TempOrderItems]
WHERE [ConsigneeName] = '{consigneeSafe}'";
//string qShowMeSumOfOrder = "select cast(sum(Price * Zakaz) as real) from TempOrderItems"; //string qShowMeSumOfOrder = "select cast(sum(Price * Zakaz) as real) from TempOrderItems";
@ -71,8 +53,7 @@ WHERE [ConsigneeName] = '{consigneeSafe}'";
if (Supplier != string.Empty && Supplier != "Корзина") if (Supplier != string.Empty && Supplier != "Корзина")
{ {
var supplierSafe = Supplier.Replace("'", "''"); qShowMeOrderItems += $" where [SupplierName] = '{Supplier}'";
qShowMeOrderItems += $" and [SupplierName] = '{supplierSafe}'";
} }
SQLiteCommand cmdShowMeTempOrderItems = new SQLiteCommand(qShowMeOrderItems, conTempOrder); SQLiteCommand cmdShowMeTempOrderItems = new SQLiteCommand(qShowMeOrderItems, conTempOrder);
@ -305,9 +286,8 @@ and ConsigneesName = '{consigneeName}'
private void DeleteCartItem(string idPriceListItem, string drugName) private void DeleteCartItem(string idPriceListItem, string drugName)
{ {
var safeId = idPriceListItem.Replace("'", "''"); var safeId = idPriceListItem.Replace("'", "''");
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{safeId}'"; string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{safeId}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{safeId}' and [ConsigneeName] = '{consigneeSafe}'"; string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{safeId}'";
using (var sqlCon = new SQLiteConnection(connectionStringToLocalDB)) using (var sqlCon = new SQLiteConnection(connectionStringToLocalDB))
{ {
@ -344,26 +324,18 @@ and ConsigneesName = '{consigneeName}'
{ {
using(SQLiteConnection sqlConForDeleteTempOrder = new SQLiteConnection(connectionStringToLocalDB)) using(SQLiteConnection sqlConForDeleteTempOrder = new SQLiteConnection(connectionStringToLocalDB))
{ {
var supplierSafe = (Supplier ?? string.Empty).Replace("'", "''");
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
string qDeletePriceListItemForASupplier; string qDeletePriceListItemForASupplier;
string qDeleteTempOrderForASupplier; string qDeleteTempOrderForASupplier;
if (supplierSafe != string.Empty) if (Supplier != string.Empty)
{ {
qDeletePriceListItemForASupplier = qDeletePriceListItemForASupplier = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where [id_PriceList_Item] in (select [id_PriceList_Item] from [TempOrderItems] where [SupplierName] = '{Supplier}')";
$"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where [id_PriceList_Item] in " + qDeleteTempOrderForASupplier = $"delete from [TempOrderItems] where [SupplierName] = '{Supplier}'";
$"(select [id_PriceList_Item] from [TempOrderItems] where [SupplierName] = '{supplierSafe}' and [ConsigneeName] = '{consigneeSafe}')";
qDeleteTempOrderForASupplier =
$"delete from [TempOrderItems] where [SupplierName] = '{supplierSafe}' and [ConsigneeName] = '{consigneeSafe}'";
} }
else else
{ {
qDeletePriceListItemForASupplier = qDeletePriceListItemForASupplier = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null";
$"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null " + qDeleteTempOrderForASupplier = $"delete from [TempOrderItems]";
$"where [id_PriceList_Item] in (select [id_PriceList_Item] from [TempOrderItems] where [ConsigneeName] = '{consigneeSafe}')";
qDeleteTempOrderForASupplier =
$"delete from [TempOrderItems] where [ConsigneeName] = '{consigneeSafe}'";
} }
@ -451,18 +423,10 @@ ConsigneesName = '{consigneeName}'
private void btnSendOrder_Click(object sender, EventArgs e) private void btnSendOrder_Click(object sender, EventArgs e)
{ {
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
if (!hasToken)
{
UiDialogs.ShowInfo("Сначала выполните авторизацию, чтобы создать заказ.", "Авторизация", FindForm());
return;
}
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string SupplierName = _supplierName; string SupplierName = _supplierName;
string consigneeName = _consigneeName; string consigneeName = _consigneeName;
var consigneeSafe = (consigneeName ?? string.Empty).Replace("'", "''");
@ -501,7 +465,6 @@ select
SupplierName SupplierName
from TempOrderItems from TempOrderItems
where zakaz > 0 where zakaz > 0
and [ConsigneeName] = '{consigneeSafe}'
{filterAddon} {filterAddon}
group by SupplierName group by SupplierName
"; ";
@ -554,12 +517,9 @@ group by SupplierName
void createOrderForNextSuppliers(string SupName) void createOrderForNextSuppliers(string SupName)
{ {
// Глобальный номер EX-0000001… (сервер / локальный fallback). string OrderNumber = $"EP-{DateTime.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid().ToString("N").Substring(0, 6)}";
string OrderNumber = OrderNumberHelper.AllocateForNewOrder(); string OrderDate = DateTime.Now.ToString("dd.MM.yyyy");
string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");
_consigneeName = ParentForm?.ConsigneeName ?? _consigneeName ?? string.Empty; _consigneeName = ParentForm?.ConsigneeName ?? _consigneeName ?? string.Empty;
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
var supNameSafe = (SupName ?? string.Empty).Replace("'", "''");
if (HasStockIssues(SupName, out var stockIssue)) if (HasStockIssues(SupName, out var stockIssue))
{ {
@ -572,16 +532,14 @@ select
count(*) count(*)
from TempOrderItems from TempOrderItems
where Zakaz > 0 where Zakaz > 0
and SupplierName = '{supNameSafe}' and SupplierName = '{SupName.Replace("'", "''")}'
and [ConsigneeName] = '{consigneeSafe}'
"; ";
string qShowMeSumOrderedItems = $@" string qShowMeSumOrderedItems = $@"
select select
ifnull(sum(cast(replace(SummaZakaza, ',', '.') as real)), 0) ifnull(sum(cast(replace(SummaZakaza, ',', '.') as real)), 0)
from TempOrderItems from TempOrderItems
where Zakaz > 0 where Zakaz > 0
and SupplierName = '{supNameSafe}' and SupplierName = '{SupName.Replace("'", "''")}'
and [ConsigneeName] = '{consigneeSafe}'
"; ";
string qShowMeMinimalSumOfOrder = $@" string qShowMeMinimalSumOfOrder = $@"
select select
@ -591,7 +549,7 @@ where SuppliersName = '{SupName.Replace("'", "''")}'
"; ";
string qDisposeTempOrder = $@" string qDisposeTempOrder = $@"
delete from TempOrderItems where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}' delete from TempOrderItems where SupplierName = '{SupName}'
"; ";
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
@ -636,13 +594,12 @@ select
'{OrderDate}', '{OrderDate}',
supplier_id, supplier_id,
SupplierName, SupplierName,
(select count(*) from TempOrderItems where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}'), (select count(*) from TempOrderItems where SupplierName = '{SupName.Replace("'", "''")}'),
'{sumOrder.ToString(System.Globalization.CultureInfo.InvariantCulture)}', '{sumOrder.ToString(System.Globalization.CultureInfo.InvariantCulture)}',
'НОВЫЙ', 'НОВЫЙ',
'{_consigneeName.Replace("'", "''")}' '{_consigneeName.Replace("'", "''")}'
from TempOrderItems from TempOrderItems
where SupplierName = '{supNameSafe}' where SupplierName = '{SupName.Replace("'", "''")}'
and [ConsigneeName] = '{consigneeSafe}'
group by supplier_id, SupplierName group by supplier_id, SupplierName
"; ";
@ -659,7 +616,7 @@ select
[ExpiryPeriod], [ExpiryPeriod],
[supplier_price_id] [supplier_price_id]
from TempOrderItems from TempOrderItems
where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}' where SupplierName = '{SupName}'
"; ";
#region старыеЗапросыСозданияЗаказа #region старыеЗапросыСозданияЗаказа

View File

@ -1,4 +1,4 @@
namespace Электроннаяармация.UserControls namespace Электроннаяармация.UserControls
{ {
partial class UCPriceList partial class UCPriceList
{ {
@ -99,7 +99,7 @@ namespace Электронная_Фармация.UserControls
this.SplitPriceList.Panel2.Controls.Add(this.POrderInfo); this.SplitPriceList.Panel2.Controls.Add(this.POrderInfo);
this.SplitPriceList.Size = new System.Drawing.Size(1053, 532); this.SplitPriceList.Size = new System.Drawing.Size(1053, 532);
this.SplitPriceList.SplitterDistance = 290; this.SplitPriceList.SplitterDistance = 290;
this.SplitPriceList.SplitterWidth = 6; this.SplitPriceList.SplitterWidth = 2;
this.SplitPriceList.TabIndex = 0; this.SplitPriceList.TabIndex = 0;
// //
// TLPPriceList // TLPPriceList

View File

@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Data; using System.Data;
@ -35,7 +35,6 @@ namespace Электронная_Фармация.UserControls
DataTable tablePriceList; DataTable tablePriceList;
string _WorkWithConsignee; string _WorkWithConsignee;
decimal _percentageAsDefault; decimal _percentageAsDefault;
bool _suppressMarkupPersist;
string _selectedSupplierFilter = string.Empty; string _selectedSupplierFilter = string.Empty;
bool _suppressSupplierFilterEvents; bool _suppressSupplierFilterEvents;
bool _suppressPriceListSelectionEvents; bool _suppressPriceListSelectionEvents;
@ -49,11 +48,6 @@ namespace Электронная_Фармация.UserControls
ModernSplitContainer splitNameMgBrowse; ModernSplitContainer splitNameMgBrowse;
ModernDataGridView dgvBrowseNames; ModernDataGridView dgvBrowseNames;
ModernDataGridView dgvBrowseMg; ModernDataGridView dgvBrowseMg;
List<string> _uniqueBaseNames = new List<string>();
Dictionary<string, List<string>> _mgByBaseName =
new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
Timer _searchDebounceTimer;
const int SearchDebounceMs = 180;
static readonly Regex DosageRegex = new Regex( static readonly Regex DosageRegex = new Regex(
@"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)", @"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled); RegexOptions.IgnoreCase | RegexOptions.Compiled);
@ -78,14 +72,13 @@ namespace Электронная_Фармация.UserControls
private void UCPriceList_Load(object sender, EventArgs e) private void UCPriceList_Load(object sender, EventArgs e)
{ {
EnsureBrowseSplit(); EnsureBrowseSplit();
EnsureChromeLayout();
UiThemeHelper.ApplyToControlTree(this); UiThemeHelper.ApplyToControlTree(this);
Tag = "chrome-child"; Tag = "chrome-child";
AlignSearchRow(); AlignSearchRow();
LayoutContentBelowSearch();
SizeChanged += (_, __) => LayoutContentBelowSearch(); panel1.SizeChanged += (_, __) => LayoutMarkupRow();
LayoutMarkupRow();
txtConsignee.Enabled = false; txtConsignee.Enabled = false;
txtConsignee.Text = _WorkWithConsignee; txtConsignee.Text = _WorkWithConsignee;
@ -98,120 +91,6 @@ namespace Электронная_Фармация.UserControls
txtSearchGoodNameInPriceList.Focus(); txtSearchGoodNameInPriceList.Focus();
} }
/// <summary>
/// Поиск остаётся сверху как в оригинале; контент явно ниже — без Dock-наложения.
/// </summary>
private void EnsureChromeLayout()
{
// Убираем старую dock-панель, если осталась от прошлых сборок в той же сессии.
var oldToolbar = Controls["priceToolbar"];
if (oldToolbar != null)
{
var toRestore = oldToolbar.Controls.Cast<Control>().ToArray();
foreach (var control in toRestore)
{
oldToolbar.Controls.Remove(control);
Controls.Add(control);
control.BringToFront();
}
Controls.Remove(oldToolbar);
oldToolbar.Dispose();
}
// Строка наценки скрыта — проценты берутся из настроек/дефолта без UI-прослойки.
if (TLPPriceList.RowStyles.Count >= 2)
{
TLPPriceList.RowStyles[1].SizeType = SizeType.Absolute;
TLPPriceList.RowStyles[1].Height = 0;
}
panel1.Visible = false;
panel1.Height = 0;
SplitPriceList.Panel1MinSize = 120;
SplitPriceList.Panel2MinSize = 80;
SplitPriceList.IsSplitterFixed = false;
SplitPriceList.FixedPanel = FixedPanel.None;
SplitPriceList.SplitterWidth = 6;
// Восстановить положение сплиттера корзины, если пользователь уже двигал.
RestoreCartSplitterDistance();
SplitPriceList.SplitterMoved += (_, __) => PersistCartSplitterDistance();
dgvPriceList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None;
dgvPriceList.ColumnHeadersVisible = true;
dgvPriceList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
if (dgvPriceList.ColumnHeadersHeight < 34)
{
dgvPriceList.ColumnHeadersHeight = 36;
}
if (!POrderInfo.Controls.ContainsKey("cartHeader"))
{
var cartHeader = new ModernSectionHeader
{
Name = "cartHeader",
Height = 32,
HeaderText = "Корзина"
};
cartHeader.ApplyTheme(Elfisa.UI.Theming.ThemeManager.Colors, cartStyle: true);
cartHeader.Dock = DockStyle.Top;
POrderInfo.Controls.Add(cartHeader);
tabOrderItems.SendToBack();
cartHeader.BringToFront();
}
}
private void LayoutContentBelowSearch()
{
const int searchBand = 52;
PContentPriceList.Dock = DockStyle.None;
PContentPriceList.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
PContentPriceList.SetBounds(
0,
searchBand,
Math.Max(100, ClientSize.Width),
Math.Max(120, ClientSize.Height - searchBand));
}
private void RestoreCartSplitterDistance()
{
try
{
var saved = Properties.Settings.Default.PriceListSplitterDistance;
if (saved <= 0 || SplitPriceList.Height < 200)
{
return;
}
var max = Math.Max(
SplitPriceList.Panel1MinSize,
SplitPriceList.Height - SplitPriceList.Panel2MinSize - SplitPriceList.SplitterWidth);
SplitPriceList.SplitterDistance = Math.Max(SplitPriceList.Panel1MinSize, Math.Min(saved, max));
}
catch
{
// ignore until layout ready
}
}
private void PersistCartSplitterDistance()
{
try
{
if (SplitPriceList.SplitterDistance > 80)
{
Properties.Settings.Default.PriceListSplitterDistance = SplitPriceList.SplitterDistance;
Properties.Settings.Default.Save();
}
}
catch
{
// ignore
}
}
private void EnsureBrowseSplit() private void EnsureBrowseSplit()
{ {
if (splitNameMgBrowse != null) if (splitNameMgBrowse != null)
@ -227,35 +106,13 @@ namespace Электронная_Фармация.UserControls
Name = "splitNameMgBrowse", Name = "splitNameMgBrowse",
Dock = DockStyle.Fill, Dock = DockStyle.Fill,
Orientation = Orientation.Vertical, Orientation = Orientation.Vertical,
SplitterWidth = 6, SplitterWidth = 4,
Visible = false, Visible = false
Panel1MinSize = 100,
Panel2MinSize = 80,
IsSplitterFixed = false,
FixedPanel = FixedPanel.None
}; };
splitNameMgBrowse.Panel1.Controls.Add(dgvBrowseNames); splitNameMgBrowse.Panel1.Controls.Add(dgvBrowseNames);
splitNameMgBrowse.Panel2.Controls.Add(dgvBrowseMg); splitNameMgBrowse.Panel2.Controls.Add(dgvBrowseMg);
var gridHost = TLPPriceList.Controls["priceGridHost"] as Panel; TLPPriceList.Controls.Add(splitNameMgBrowse, 0, 0);
if (gridHost == null)
{
gridHost = new Panel
{
Name = "priceGridHost",
Dock = DockStyle.Fill,
Margin = new Padding(0),
Padding = new Padding(0),
Tag = "chrome-child"
};
TLPPriceList.Controls.Remove(dgvPriceList);
dgvPriceList.Dock = DockStyle.Fill;
gridHost.Controls.Add(dgvPriceList);
TLPPriceList.Controls.Add(gridHost, 0, 0);
}
gridHost.Controls.Add(splitNameMgBrowse);
splitNameMgBrowse.BringToFront(); splitNameMgBrowse.BringToFront();
dgvBrowseNames.SelectionChanged += dgvBrowseNames_SelectionChanged; dgvBrowseNames.SelectionChanged += dgvBrowseNames_SelectionChanged;
@ -294,13 +151,11 @@ namespace Электронная_Фармация.UserControls
private void AlignSearchRow() private void AlignSearchRow()
{ {
const int searchY = 10; const int searchY = 9;
const int searchHeight = 32; const int searchHeight = 32;
const int clearGap = 4; const int clearGap = 2;
const int leftPad = 10;
label2.Location = new Point(leftPad, searchY + 4); txtSearchGoodNameInPriceList.SetBounds(78, searchY, 313, searchHeight);
txtSearchGoodNameInPriceList.SetBounds(label2.Right + 6, searchY, 300, searchHeight);
btnClearSearchInPriceList.SetBounds( btnClearSearchInPriceList.SetBounds(
txtSearchGoodNameInPriceList.Right + clearGap, txtSearchGoodNameInPriceList.Right + clearGap,
searchY, searchY,
@ -308,23 +163,38 @@ namespace Электронная_Фармация.UserControls
searchHeight); searchHeight);
ModernButtonStyles.ApplyClear(btnClearSearchInPriceList); ModernButtonStyles.ApplyClear(btnClearSearchInPriceList);
lblSupplierFilter.Location = new Point(btnClearSearchInPriceList.Right + 16, searchY + 4);
comboSupplierFilter.SetBounds(lblSupplierFilter.Right + 6, searchY, 240, searchHeight);
btnClearSupplierFilter.SetBounds(comboSupplierFilter.Right + clearGap, searchY, 32, searchHeight);
btnClearSupplierFilter.Height = searchHeight; btnClearSupplierFilter.Height = searchHeight;
// Поиск всегда поверх контента, но контент начинается ниже searchBand.
label2.BringToFront();
txtSearchGoodNameInPriceList.BringToFront();
btnClearSearchInPriceList.BringToFront();
lblSupplierFilter.BringToFront();
comboSupplierFilter.BringToFront();
btnClearSupplierFilter.BringToFront();
} }
private void LayoutMarkupRow() private void LayoutMarkupRow()
{ {
// UI наценки скрыт; метод оставлен на случай повторного включения. // Keep markup row readable at different DPI/window sizes.
try
{
var paddingLeft = 8;
var y = 30;
var gap = 8;
label4.Location = new Point(paddingLeft, y + 2);
numericPercent.Location = new Point(label4.Right + 6, y);
numericPercent.Width = 56;
checkUsePercentageAsDefault.AutoSize = true;
checkUsePercentageAsDefault.Location = new Point(
Math.Max(numericPercent.Right + gap, panel1.ClientSize.Width - checkUsePercentageAsDefault.PreferredSize.Width - 10),
y + 2);
lblPriceForGood.AutoSize = false;
lblPriceForGood.AutoEllipsis = true;
lblPriceForGood.Location = new Point(numericPercent.Right + gap, y + 2);
lblPriceForGood.Height = 22;
lblPriceForGood.Width = Math.Max(90, checkUsePercentageAsDefault.Left - lblPriceForGood.Left - gap);
}
catch
{
// best-effort layout; ignore
}
} }
private void InitializeSupplierFilter() private void InitializeSupplierFilter()
@ -536,77 +406,11 @@ namespace Электронная_Фармация.UserControls
EnsureDerivedBrowseColumns(tablePrice); EnsureDerivedBrowseColumns(tablePrice);
_fullPriceTable = tablePrice; _fullPriceTable = tablePrice;
RebuildBrowseIndex(tablePrice);
_selectedBaseName = string.Empty; _selectedBaseName = string.Empty;
_selectedMg = string.Empty; _selectedMg = string.Empty;
ShowBrowseView(); ShowBrowseView();
} }
private void RebuildBrowseIndex(DataTable tablePrice)
{
var names = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
var mgMap = new Dictionary<string, SortedSet<string>>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in tablePrice.Rows)
{
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(baseName))
{
continue;
}
names.Add(baseName);
if (!mgMap.TryGetValue(baseName, out var dosages))
{
dosages = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
mgMap[baseName] = dosages;
}
dosages.Add(row["МГ"]?.ToString() ?? string.Empty);
}
_uniqueBaseNames = names.ToList();
_mgByBaseName = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
foreach (var pair in mgMap)
{
var list = new List<string>(pair.Value.Count);
foreach (var dosage in pair.Value)
{
list.Add(string.IsNullOrEmpty(dosage) ? EmptyMgDisplay : dosage);
}
_mgByBaseName[pair.Key] = list;
}
}
private void EnsureSearchDebounceTimer()
{
if (_searchDebounceTimer != null)
{
return;
}
_searchDebounceTimer = new Timer { Interval = SearchDebounceMs };
_searchDebounceTimer.Tick += (_, __) =>
{
_searchDebounceTimer.Stop();
ResetBrowseToNames();
};
}
private void ScheduleBrowseSearchRefresh(bool immediate = false)
{
EnsureSearchDebounceTimer();
_searchDebounceTimer.Stop();
if (immediate)
{
ResetBrowseToNames();
return;
}
_searchDebounceTimer.Start();
}
private void HideOptionalColumn(string columnName) private void HideOptionalColumn(string columnName)
{ {
if (dgvPriceList.Columns.Contains(columnName)) if (dgvPriceList.Columns.Contains(columnName))
@ -701,51 +505,55 @@ namespace Электронная_Фармация.UserControls
} }
var search = (GoodsFilter ?? string.Empty).Trim(); var search = (GoodsFilter ?? string.Empty).Trim();
IList<string> namesToShow; var namesTable = new DataTable();
if (string.IsNullOrWhiteSpace(search)) namesTable.Columns.Add("Наименование", typeof(string));
{
namesToShow = _uniqueBaseNames; var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
else
{
var matching = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in _fullPriceTable.Rows) foreach (DataRow row in _fullPriceTable.Rows)
{ {
var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty; var baseName = row["Базовое наименование"]?.ToString() ?? string.Empty;
if (string.IsNullOrWhiteSpace(baseName) || matching.Contains(baseName)) if (string.IsNullOrWhiteSpace(baseName) || !seen.Add(baseName))
{ {
continue; continue;
} }
var drugName = row["Наименование"]?.ToString() ?? string.Empty; if (!string.IsNullOrWhiteSpace(search))
var mgText = row["МГ"]?.ToString() ?? string.Empty; {
var keep = false;
foreach (DataRow candidate in _fullPriceTable.Rows)
{
if (!string.Equals(candidate["Базовое наименование"]?.ToString(), baseName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var drugName = candidate["Наименование"]?.ToString() ?? string.Empty;
var mgText = candidate["МГ"]?.ToString() ?? string.Empty;
if (baseName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0 if (baseName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| drugName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0 || drugName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0
|| mgText.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) || mgText.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0)
{ {
matching.Add(baseName); keep = true;
break;
} }
} }
namesToShow = matching if (!keep)
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToList();
}
var namesTable = new DataTable();
namesTable.Columns.Add("Наименование", typeof(string));
namesTable.BeginLoadData();
foreach (var baseName in namesToShow)
{ {
continue;
}
}
namesTable.Rows.Add(baseName); namesTable.Rows.Add(baseName);
} }
namesTable.EndLoadData();
namesTable.DefaultView.Sort = "Наименование ASC";
var sortedNames = namesTable.DefaultView.ToTable();
try try
{ {
_suppressBrowseEvents = true; _suppressBrowseEvents = true;
dgvBrowseNames.SuspendLayout(); dgvBrowseNames.DataSource = sortedNames;
dgvBrowseNames.DataSource = namesTable;
ConfigureSingleColumnGrid(dgvBrowseNames, "Наименование"); ConfigureSingleColumnGrid(dgvBrowseNames, "Наименование");
ClearMgPanel(); ClearMgPanel();
@ -760,7 +568,6 @@ namespace Электронная_Фармация.UserControls
} }
finally finally
{ {
dgvBrowseNames.ResumeLayout();
_suppressBrowseEvents = false; _suppressBrowseEvents = false;
} }
@ -787,18 +594,28 @@ namespace Электронная_Фармация.UserControls
var mgTable = new DataTable(); var mgTable = new DataTable();
mgTable.Columns.Add("МГ", typeof(string)); mgTable.Columns.Add("МГ", typeof(string));
if (_mgByBaseName.TryGetValue(baseName, out var dosages)) var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (DataRow row in _fullPriceTable.Rows)
{ {
foreach (var dosage in dosages) if (!string.Equals(row["Базовое наименование"]?.ToString(), baseName, StringComparison.OrdinalIgnoreCase))
{ {
mgTable.Rows.Add(dosage); continue;
}
} }
var dosage = row["МГ"]?.ToString() ?? string.Empty;
if (!seen.Add(dosage))
{
continue;
}
mgTable.Rows.Add(string.IsNullOrEmpty(dosage) ? EmptyMgDisplay : dosage);
}
mgTable.DefaultView.Sort = "МГ ASC";
try try
{ {
_suppressBrowseEvents = true; _suppressBrowseEvents = true;
dgvBrowseMg.DataSource = mgTable; dgvBrowseMg.DataSource = mgTable.DefaultView.ToTable();
ConfigureSingleColumnGrid(dgvBrowseMg, "МГ"); ConfigureSingleColumnGrid(dgvBrowseMg, "МГ");
dgvBrowseMg.ClearSelection(); dgvBrowseMg.ClearSelection();
} }
@ -849,21 +666,10 @@ namespace Электронная_Фармация.UserControls
{ {
EnsureBrowseSplit(); EnsureBrowseSplit();
var gridHost = TLPPriceList.Controls["priceGridHost"] as Panel;
if (gridHost != null)
{
gridHost.Dock = DockStyle.Fill;
if (gridHost.Padding.Top < 8)
{
gridHost.Padding = new Padding(0, 8, 0, 0);
}
}
if (browseVisible) if (browseVisible)
{ {
dgvPriceList.Visible = false; dgvPriceList.Visible = false;
splitNameMgBrowse.Visible = true; splitNameMgBrowse.Visible = true;
splitNameMgBrowse.Dock = DockStyle.Fill;
splitNameMgBrowse.BringToFront(); splitNameMgBrowse.BringToFront();
var width = Math.Max(200, splitNameMgBrowse.Width); var width = Math.Max(200, splitNameMgBrowse.Width);
@ -889,9 +695,6 @@ namespace Электронная_Фармация.UserControls
splitNameMgBrowse.Visible = false; splitNameMgBrowse.Visible = false;
dgvPriceList.Visible = true; dgvPriceList.Visible = true;
dgvPriceList.Dock = DockStyle.Fill;
dgvPriceList.ColumnHeadersVisible = true;
dgvPriceList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None;
dgvPriceList.BringToFront(); dgvPriceList.BringToFront();
} }
} }
@ -1186,7 +989,7 @@ namespace Электронная_Фармация.UserControls
GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1); GoodsFilter = GoodsFilter.Substring(0, GoodsFilter.Length - 1);
txtSearchGoodNameInPriceList.Text = GoodsFilter; txtSearchGoodNameInPriceList.Text = GoodsFilter;
ScheduleBrowseSearchRefresh(immediate: GoodsFilter.Length == 0); ResetBrowseToNames();
if (GoodsFilter.Length == 0) if (GoodsFilter.Length == 0)
{ {
@ -1203,59 +1006,34 @@ namespace Электронная_Фармация.UserControls
void loadInfoAboutDefaultMarkup() void loadInfoAboutDefaultMarkup()
{ {
decimal percentageMarkupDefault = 0m; decimal percentageMarkupDefault = Properties.Settings.Default.ClientMarkupPercent;
var fromConsignee = ConsigneeHelper.GetClientMarkupPercent(_WorkWithConsignee);
if (fromConsignee.HasValue)
{
percentageMarkupDefault = fromConsignee.Value;
}
else
{
percentageMarkupDefault = Properties.Settings.Default.ClientMarkupPercent;
if (percentageMarkupDefault <= 0) if (percentageMarkupDefault <= 0)
{ {
percentageMarkupDefault = Properties.Settings.Default.IntDefaultPercentMarkup; percentageMarkupDefault = Properties.Settings.Default.IntDefaultPercentMarkup;
} }
if (percentageMarkupDefault < 0) if (percentageMarkupDefault <= 0)
{ {
percentageMarkupDefault = MarkupHelper.FallbackMarkupPercent; percentageMarkupDefault = MarkupHelper.FallbackMarkupPercent;
} }
// Первый запуск для этой аптеки — сразу закрепим текущее значение за аптекой.
if (!string.IsNullOrWhiteSpace(_WorkWithConsignee))
{
try
{
ConsigneeHelper.SetClientMarkupPercent(_WorkWithConsignee, percentageMarkupDefault);
}
catch
{
// колонка может ещё не быть — при следующем старте миграция добавит
}
}
}
_percentageAsDefault = percentageMarkupDefault; _percentageAsDefault = percentageMarkupDefault;
_suppressMarkupPersist = true;
try
{
numericPercent.DecimalPlaces = 2; numericPercent.DecimalPlaces = 2;
numericPercent.Increment = 0.1m; numericPercent.Increment = 0.1m;
numericPercent.Value = Math.Max( numericPercent.Value = Math.Max(numericPercent.Minimum, Math.Min(numericPercent.Maximum, percentageMarkupDefault));
numericPercent.Minimum,
Math.Min(numericPercent.Maximum, percentageMarkupDefault));
}
finally
{
_suppressMarkupPersist = false;
}
} }
private decimal GetMarkupPercentForRow(DataGridViewRow row) private decimal GetMarkupPercentForRow(DataGridViewRow row)
{ {
// Для превью/спиннера всегда используем процент аптеки, который задал клиент. if (row?.DataGridView?.Columns.Contains("Наценка %") == true)
// Серверные значения в колонке «Наценка %» не перетирают ручной ввод. {
var cellValue = row.Cells["Наценка %"]?.Value;
if (MarkupHelper.TryParseMarkup(cellValue, out var itemMarkup) && itemMarkup >= 0)
{
return itemMarkup;
}
}
return _percentageAsDefault; return _percentageAsDefault;
} }
@ -1294,12 +1072,10 @@ namespace Электронная_Фармация.UserControls
List<String> SuppliersFromOrder = new List<String>(); List<String> SuppliersFromOrder = new List<String>();
var consigneeSafe = (_WorkWithConsignee ?? string.Empty).Replace("'", "''");
string qShowMeSuppliersFromOrder = $@" string qShowMeSuppliersFromOrder = $@"
select select
[SupplierName] [SupplierName]
from [TempOrderItems] from [TempOrderItems]
where [ConsigneeName] = '{consigneeSafe}'
group by [SupplierName] group by [SupplierName]
"; ";
@ -1521,7 +1297,7 @@ BestBefore = '{BestBefore}'
{ {
string connectionStringToLocalDB = AppConfig.SqliteConnectionString; string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
string OrderNumber = OrderNumberHelper.AllocateForNewOrder(); string OrderNumber = $"EP-{DateTime.Now.Millisecond.ToString()}{DateTime.Now.Date.ToString("mmdd")}";
string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");//DateTime.Now.ToShortDateString(); string OrderDate = DateTime.Now.ToString("yyyy-MM-dd");//DateTime.Now.ToShortDateString();
@ -1697,7 +1473,7 @@ select strftime('%Y-%m-%d','{OrderDate}'),
{ {
txtSearchGoodNameInPriceList.Text = string.Empty; txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty; GoodsFilter = string.Empty;
ScheduleBrowseSearchRefresh(immediate: true); ResetBrowseToNames();
//loadPriceList(string.Empty); //loadPriceList(string.Empty);
} }
@ -1756,27 +1532,6 @@ and tempOrder.GoodName = PriceList.GoodName
string GoodsFilter = string.Empty; string GoodsFilter = string.Empty;
string stringIWantBuy = string.Empty; string stringIWantBuy = string.Empty;
private static bool IsGoodsSearchKey(char keyChar, bool allowDigitsAndSeparators = true)
{
if ((keyChar >= 'A' && keyChar <= 'Z')
|| (keyChar >= 'a' && keyChar <= 'z')
|| (keyChar >= 'А' && keyChar <= 'Я')
|| (keyChar >= 'а' && keyChar <= 'я')
|| keyChar == (char)Keys.Subtract)
{
return true;
}
if (!allowDigitsAndSeparators)
{
return false;
}
return (keyChar >= '0' && keyChar <= '9')
|| keyChar == ' '
|| keyChar == '.'
|| keyChar == ',';
}
private void dgvPriceList_KeyPress(object sender, KeyPressEventArgs e) private void dgvPriceList_KeyPress(object sender, KeyPressEventArgs e)
{ {
@ -1790,7 +1545,7 @@ and tempOrder.GoodName = PriceList.GoodName
{ {
txtSearchGoodNameInPriceList.Text = string.Empty; txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty; GoodsFilter = string.Empty;
ScheduleBrowseSearchRefresh(immediate: true); ResetBrowseToNames();
} }
return; return;
@ -1830,27 +1585,67 @@ and tempOrder.GoodName = PriceList.GoodName
if (_browseMode != PriceBrowseMode.Offers) if (_browseMode != PriceBrowseMode.Offers)
{ {
if (IsGoodsSearchKey(e.KeyChar)) 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))
{ {
dgvPriceList.ClearSelection(); dgvPriceList.ClearSelection();
GoodsFilter += e.KeyChar.ToString(); GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter; txtSearchGoodNameInPriceList.Text = GoodsFilter;
ScheduleBrowseSearchRefresh(); ResetBrowseToNames();
timerForResetSearch.Enabled = true; if (dgvBrowseNames != null && dgvBrowseNames.Rows.Count == 0)
timerForResetSearch.Start(); {
UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm());
GoodsFilter = string.Empty;
txtSearchGoodNameInPriceList.Text = string.Empty;
ResetBrowseToNames();
}
} }
return; return;
} }
if (IsGoodsSearchKey(e.KeyChar, allowDigitsAndSeparators: false)) 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))
{
if (e.KeyChar == (Char)Keys.Enter)
{
}
else
{ {
dgvPriceList.ClearSelection(); dgvPriceList.ClearSelection();
GoodsFilter += e.KeyChar.ToString(); GoodsFilter += e.KeyChar.ToString();
txtSearchGoodNameInPriceList.Text = GoodsFilter; txtSearchGoodNameInPriceList.Text = GoodsFilter;
ScheduleBrowseSearchRefresh();
timerForResetSearch.Enabled = true; #region поиск без фильтрации (НЕ РАБОТАЕТ)
timerForResetSearch.Start(); //foreach(DataGridViewRow row in dgvPriceList.Rows)
//{
// //string cellValue = row.Cells["Наименование"].Value.ToString().ToLower();
// if (row.Cells[2].Value.ToString().Equals(GoodsFilter))
// {
// int indexOfSearchValue = row.Index;
// dgvPriceList.Rows[indexOfSearchValue].Selected = true;
// }
//}
#endregion
#region старый способ поиска с фильтрацией данных
if (!string.IsNullOrEmpty(GoodsFilter))
{
ResetBrowseToNames();
if (dgvPriceList.Rows.Count == 0)
{
UiDialogs.ShowInfo("Подходящий товар не найден.", "Поиск", FindForm());
GoodsFilter = string.Empty;
txtSearchGoodNameInPriceList.Text = string.Empty;
ResetBrowseToNames();
}
}
#endregion
}
return; return;
} }
else if ((e.KeyChar >= (Char)Keys.D1 && e.KeyChar <= (Char)Keys.D9) || else if ((e.KeyChar >= (Char)Keys.D1 && e.KeyChar <= (Char)Keys.D9) ||
@ -1964,10 +1759,9 @@ and tempOrder.GoodName = PriceList.GoodName
void creatingTempOrder(string idPriceListItem, string zakaz, string summaZakaza) void creatingTempOrder(string idPriceListItem, string zakaz, string summaZakaza)
{ {
var consigneeSafe = (_WorkWithConsignee ?? string.Empty).Replace("'", "''");
string qUpdatePriceList = $"update [PriceList] set [Zakaz] = '{zakaz}', [SummaZakaza] = '{summaZakaza}' where [id_PriceList_Item] = '{idPriceListItem}'"; string qUpdatePriceList = $"update [PriceList] set [Zakaz] = '{zakaz}', [SummaZakaza] = '{summaZakaza}' where [id_PriceList_Item] = '{idPriceListItem}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{idPriceListItem}' and [ConsigneeName] = '{consigneeSafe}'"; string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{idPriceListItem}'";
string qCreateNewTempOrder = $"insert into TempOrderItems([ConsigneeName],[guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id]) select '{consigneeSafe}',[guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id] from [PriceList] where [id_PriceList_Item] = '{idPriceListItem}'"; string qCreateNewTempOrder = $"insert into TempOrderItems([guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id]) select [guid_es],[es_code],[supplier_id],[DrugName],[SupplierName],[Price],[Quantity],[ExpiryPeriod],[Description],[Zakaz],[SummaZakaza],[id_PriceList_Item],[supplier_price_id] from [PriceList] where [id_PriceList_Item] = '{idPriceListItem}'";
using(SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB)) using(SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
{ {
@ -2001,14 +1795,13 @@ and tempOrder.GoodName = PriceList.GoodName
void deleteGood(int index, string IdPriceListItem) void deleteGood(int index, string IdPriceListItem)
{ {
var consigneeSafe = (_WorkWithConsignee ?? string.Empty).Replace("'", "''");
dgvPriceList.Rows[index].Cells[9].Value = string.Empty; dgvPriceList.Rows[index].Cells[9].Value = string.Empty;
dgvPriceList.Rows[index].Cells[10].Value = string.Empty; dgvPriceList.Rows[index].Cells[10].Value = string.Empty;
string IdPLI = IdPriceListItem; string IdPLI = IdPriceListItem;
string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{IdPLI}'"; string qClearPriceList = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where id_PriceList_Item = '{IdPLI}'";
string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{IdPLI}' and [ConsigneeName] = '{consigneeSafe}'"; string qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{IdPLI}'";
using(SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB)) using(SQLiteConnection sqlCon = new SQLiteConnection(connectionStringToLocalDB))
{ {
@ -2231,19 +2024,9 @@ and SumOrderedItems = '{SumOrder}'";
} }
var markupPercentage = GetMarkupPercentForRow(currentRow); var markupPercentage = GetMarkupPercentForRow(currentRow);
_suppressMarkupPersist = true; numericPercent.Value = Math.Max(numericPercent.Minimum, Math.Min(numericPercent.Maximum, markupPercentage));
try var priceWithPercentage = MarkupHelper.ApplyMarkup((decimal)priceForGood, markupPercentage);
{ lblPriceForGood.Text = $"= {priceWithPercentage:0.##} руб";
numericPercent.Value = Math.Max(
numericPercent.Minimum,
Math.Min(numericPercent.Maximum, markupPercentage));
}
finally
{
_suppressMarkupPersist = false;
}
lblPriceForGood.Text = $"= {priceForGood:0.##} руб";
} }
catch catch
{ {
@ -2619,11 +2402,6 @@ and SumOrderedItems = '{SumOrder}'";
private void numericPercent_ValueChanged(object sender, EventArgs e) private void numericPercent_ValueChanged(object sender, EventArgs e)
{ {
if (!_suppressMarkupPersist)
{
PersistClientMarkupPercent();
}
if (numericPercent.Value != _percentageAsDefault) if (numericPercent.Value != _percentageAsDefault)
{ {
checkUsePercentageAsDefault.Checked = false; checkUsePercentageAsDefault.Checked = false;
@ -2632,30 +2410,6 @@ and SumOrderedItems = '{SumOrder}'";
ShowMePriceWithMarkupPercentForSelectedGood(); ShowMePriceWithMarkupPercentForSelectedGood();
} }
private void PersistClientMarkupPercent()
{
var value = numericPercent.Value;
_percentageAsDefault = value;
if (!string.IsNullOrWhiteSpace(_WorkWithConsignee))
{
try
{
ConsigneeHelper.SetClientMarkupPercent(_WorkWithConsignee, value);
}
catch
{
// ignore transient DB errors; value stays in UI
}
}
// Глобальный fallback на случай новой аптеки без своего значения.
Properties.Settings.Default.ClientMarkupPercent = value;
Properties.Settings.Default.IntDefaultPercentMarkup =
Convert.ToInt32(Math.Round(value, MidpointRounding.AwayFromZero));
Properties.Settings.Default.Save();
}
private void UCPriceList_Paint(object sender, PaintEventArgs e) private void UCPriceList_Paint(object sender, PaintEventArgs e)
{ {
if (comboSupplierFilter.DroppedDown) if (comboSupplierFilter.DroppedDown)
@ -2678,9 +2432,13 @@ and SumOrderedItems = '{SumOrder}'";
private void checkUsePercentageAsDefault_CheckedChanged(object sender, EventArgs e) private void checkUsePercentageAsDefault_CheckedChanged(object sender, EventArgs e)
{ {
if (checkUsePercentageAsDefault.Checked) if (_percentageAsDefault != numericPercent.Value && checkUsePercentageAsDefault.Checked == true)
{ {
PersistClientMarkupPercent(); var newDefaultPercent = numericPercent.Value;
Properties.Settings.Default.ClientMarkupPercent = newDefaultPercent;
Properties.Settings.Default.IntDefaultPercentMarkup = Convert.ToInt32(Math.Round(newDefaultPercent, MidpointRounding.AwayFromZero));
Properties.Settings.Default.Save();
_percentageAsDefault = newDefaultPercent;
} }
} }
@ -2688,7 +2446,7 @@ and SumOrderedItems = '{SumOrder}'";
{ {
txtSearchGoodNameInPriceList.Text = string.Empty; txtSearchGoodNameInPriceList.Text = string.Empty;
GoodsFilter = string.Empty; GoodsFilter = string.Empty;
ScheduleBrowseSearchRefresh(immediate: true); ResetBrowseToNames();
timerForResetSearch.Enabled = false; timerForResetSearch.Enabled = false;
timerForResetSearch.Stop(); timerForResetSearch.Stop();
} }
@ -2735,5 +2493,3 @@ and SumOrderedItems = '{SumOrder}'";
//} //}
} }
} }

View File

@ -124,7 +124,12 @@ namespace Электронная_Фармация.UserControls
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString)) using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
{ {
con.Open(); con.Open();
using (var cmd = new SQLiteCommand(GetQueryForNewSchema(), con))
var orderColumns = GetColumns(con, "Orders");
var usesNewSchema = orderColumns.Contains("Id_Order") && orderColumns.Contains("OrderedCountItems");
var q = usesNewSchema ? GetQueryForNewSchema() : GetQueryForOldSchema();
using (var cmd = new SQLiteCommand(q, con))
using (var da = new SQLiteDataAdapter(cmd)) using (var da = new SQLiteDataAdapter(cmd))
{ {
var table = new DataTable(); var table = new DataTable();
@ -181,6 +186,26 @@ order by oi.[DrugName]", con))
} }
} }
} }
private static HashSet<string> GetColumns(SQLiteConnection con, string tableName)
{
var columns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
using (var cmd = new SQLiteCommand($"pragma table_info([{tableName}]);", con))
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
var name = reader["name"] as string;
if (!string.IsNullOrWhiteSpace(name))
{
columns.Add(name);
}
}
}
return columns;
}
private static string GetQueryForNewSchema() private static string GetQueryForNewSchema()
{ {
return @" return @"
@ -212,5 +237,26 @@ group by
having sum(ifnull(oi.[RefuseItemsCount], 0)) > 0 having sum(ifnull(oi.[RefuseItemsCount], 0)) > 0
order by o.[OrderDate] desc;"; order by o.[OrderDate] desc;";
} }
private static string GetQueryForOldSchema()
{
return @"
select
o.[OrderNumber] as [Номер заказа],
o.[OrderDate] as [Дата],
supp.[SuppliersName] as [Поставщик],
consign.[ConsigneesName] as [Грузополучатель],
o.[OrderedItemsCount] as [К-во товара в заказе],
o.[OrderSum] as [Сумма заказа],
o.[RefuseItemsCount] as [К-во отказанного товара],
o.[RefuseSum] as [Сумма отказа],
o.[Comment] as [Комментарий],
o.[OrderState] as [Статус]
from Orders as o
inner join Consignees consign on consign.codeConsignees = o.codeConsignees
inner join Suppliers supp on supp.codeSuppliers = o.codeSuppliers
where ifnull(o.[RefuseItemsCount], 0) > 0 or ifnull(o.[RefuseSum], 0) > 0
order by [OrderDate] desc;";
}
} }
} }

View File

@ -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}";
}
}
}

View File

@ -1,52 +0,0 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Elfisa.Avalonia.App"
xmlns:local="using:Elfisa.Avalonia"
RequestedThemeVariant="Light">
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="Bg" Color="#EDF2F2"/>
<SolidColorBrush x:Key="Surface" Color="#FFFFFF"/>
<SolidColorBrush x:Key="Text" Color="#1F2933"/>
<SolidColorBrush x:Key="Muted" Color="#6B7785"/>
<SolidColorBrush x:Key="Border" Color="#E1E7E8"/>
<SolidColorBrush x:Key="RowAlt" Color="#F5F9F9"/>
<SolidColorBrush x:Key="InputBg" Color="#FFFFFF"/>
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="Bg" Color="#0E1416"/>
<SolidColorBrush x:Key="Surface" Color="#182226"/>
<SolidColorBrush x:Key="Text" Color="#E7EDEE"/>
<SolidColorBrush x:Key="Muted" Color="#8FA0A5"/>
<SolidColorBrush x:Key="Border" Color="#28353A"/>
<SolidColorBrush x:Key="RowAlt" Color="#141D20"/>
<SolidColorBrush x:Key="InputBg" Color="#0F1719"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
<!-- Акцент бренда (#0F766E — бирюза шапки), постоянен в обеих темах -->
<Color x:Key="AccentColor">#0F766E</Color>
<SolidColorBrush x:Key="Accent" Color="#0F766E"/>
<SolidColorBrush x:Key="AccentHover" Color="#0D9488"/>
<SolidColorBrush x:Key="AccentPressed" Color="#115E59"/>
<SolidColorBrush x:Key="AccentSoft" Color="#CCFBF1"/>
<SolidColorBrush x:Key="Sidebar" Color="#0E5F58"/>
<SolidColorBrush x:Key="SidebarActive" Color="#0A4A44"/>
<SolidColorBrush x:Key="SidebarText" Color="#D5F5F0"/>
<SolidColorBrush x:Key="OnAccent" Color="#FFFFFF"/>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="/Styles/Elfisa.axaml"/>
</Application.Styles>
</Application>

View File

@ -1,35 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System.Linq;
using Avalonia.Markup.Xaml;
using Elfisa.Avalonia.ViewModels;
using Elfisa.Avalonia.Views;
namespace Elfisa.Avalonia;
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
RequestedThemeVariant = Elfisa.Core.SettingsStore.Current.Theme == "Dark"
? global::Avalonia.Styling.ThemeVariant.Dark
: global::Avalonia.Styling.ThemeVariant.Light;
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new RootViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

View File

@ -1,38 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>2.0.1</Version>
<AssemblyVersion>2.0.1.0</AssemblyVersion>
<FileVersion>2.0.1.0</FileVersion>
<Product>ЭльФиСА — Электронная Фармация</Product>
<Company>PharmData</Company>
<!-- Кросс-платформенная публикация задаётся флагами CLI (RID + self-contained). -->
<SatelliteResourceLanguages>ru;en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="12.1.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.0" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.0" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.0" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.0" />
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Elfisa.Core\Elfisa.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -1,24 +0,0 @@
using Avalonia;
using System;
namespace Elfisa.Avalonia;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
#if DEBUG
.WithDeveloperTools()
#endif
.WithInterFont()
.LogToTrace();
}

View File

@ -1,73 +0,0 @@
# ЭльФиСА — кросс-платформенный клиент (Avalonia)
Порт десктопного клиента с WinForms на **Avalonia UI** (.NET 8) —
работает на **Windows, macOS и Linux** из одного кода. Начат как замена
WinForms-версии (которая только под Windows).
## Что уже готово (фундамент + эталонный экран)
- **Elfisa.Core** — переносимая бизнес-логика (без UI): `ApiClient`
(логин, отчёт покупателя, ИИ-отчёт), DTO, `AppConfig`, `Session`.
- **Дизайн-система** в фирменной бирюзе (#0F766E): светлая/тёмная тема,
стили кнопок, полей, карточек, таблицы, сайдбара (`Styles/Elfisa.axaml`).
- **Экран входа** (`LoginView`) — реальная авторизация через `/auth/login`.
- **Шелл** (`ShellView`) — сайдбар-навигация + карточка пользователя + выход.
- **Отчёты** (`ReportsView`) — ПОЛНОСТЬЮ рабочий эталонный раздел:
период, 4 типа отчёта (агрегация как в WinForms), авто-загрузка,
свободный **ИИ-запрос → Word/Excel** (через `/ai`), таблица с динамическими
колонками. Данные скоупятся по JWT — только заказы этого покупателя.
- Заглушки остальных разделов (Прайс-лист, Заказы, Накладные, Отправить,
Отказы, Справочник).
## Архитектура
MVVM (CommunityToolkit.Mvvm) + ViewLocator (VM → View по имени).
```
Program → App → MainWindow(RootViewModel)
RootViewModel: LoginViewModel ⇄ ShellViewModel
ShellViewModel.CurrentPage: ReportsViewModel | PlaceholderPageViewModel | …
```
Вся сетевая логика — в `Elfisa.Core` (переиспользуется и десктопом, и
будущим мобильным/веб-клиентом Avalonia).
## Запуск
```bash
dotnet run --project src/Elfisa.Avalonia
```
Dev-автологин (удобно при разработке — минуя экран входа):
```bash
# Windows PowerShell
$env:ELF_DEV_USER="user@example.com"; $env:ELF_DEV_PASS="***"; dotnet run --project src/Elfisa.Avalonia
```
Переопределение адресов (по умолчанию https://24pharmdata.ru и .../ai):
`ELF_API_BASE_URL`, `ELF_AI_BASE_URL`.
Сборка под конкретную ОС:
```bash
dotnet publish src/Elfisa.Avalonia -c Release -r osx-arm64 --self-contained # Mac (M-серия)
dotnet publish src/Elfisa.Avalonia -c Release -r win-x64 --self-contained # Windows
```
## Дорожная карта портирования
Логика этих экранов частично уже есть в WinForms `ApiClient` — переносится
в `Elfisa.Core` метод за методом, UI пишется по образцу `ReportsView`.
| Раздел | Что нужно | Сложность |
|---|---|---|
| ✅ Прайс-лист | поиск + грид (/api/supplier-prices/summary) | готово (без добавления в заказ) |
| ✅ Заказы | список + карточка заказа с позициями | готово |
| ✅ Накладные | реестр приходных документов | готово |
| ✅ Отчёты | 4 типа + ИИ → Word/Excel | готово |
| ✅ Справочник | профиль + грузополучатели | готово |
| ✅ Отправить | поиск → корзина → создание заказа (POST /api/buyer/orders) | готов UI; реальная отправка не тестировалась (создаёт заказ на проде) |
| ⏳ Отказы | список отказных позиций | нужен серверный эндпоинт (RefuseItemsCount есть только в локальной БД WinForms) |
| ⏳ Автообновление | аналог AutoUpdater под каждую ОС | не начато |
**Прайс-лист/Заказы/Накладные** строятся на рабочих эндпоинтах
(`supplier-prices/summary`, `buyer/report`). «Родные» `buyer/orders` и
`buyer/catalog` возвращают 500 (баг квотинга PascalCase в GORM-билдере).
Фикс для **buyer/orders** готов и застейджен (`es_api_service.exe.new`,
apply-new-exe.bat) — после деплоя Заказы/Накладные перейдут на него.
Каталог не используется (берём рабочий summary), его фикс — по желанию.
Осталось: **Отправить** (корзина + создание заказа + DBF) и **Отказы**,
плюс автообновление и сохранение сессии. Логика переносится из WinForms,
переписывается только UI (XAML).

View File

@ -1,193 +0,0 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- ============ Типографика ============ -->
<Style Selector="TextBlock.h1">
<Setter Property="FontSize" Value="22"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{DynamicResource Text}"/>
</Style>
<Style Selector="TextBlock.h2">
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{DynamicResource Text}"/>
</Style>
<Style Selector="TextBlock.muted">
<Setter Property="Foreground" Value="{DynamicResource Muted}"/>
<Setter Property="FontSize" Value="12.5"/>
</Style>
<Style Selector="TextBlock.label">
<Setter Property="Foreground" Value="{DynamicResource Text}"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- ============ Карточка ============ -->
<Style Selector="Border.card">
<Setter Property="Background" Value="{DynamicResource Surface}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Border}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Padding" Value="18"/>
<Setter Property="BoxShadow" Value="0 2 14 0 #12000000"/>
</Style>
<!-- ============ Кнопки ============ -->
<Style Selector="Button.primary">
<Setter Property="Background" Value="{DynamicResource Accent}"/>
<Setter Property="Foreground" Value="{DynamicResource OnAccent}"/>
<Setter Property="Padding" Value="18,10"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="13.5"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
<Style Selector="Button.primary:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentHover}"/>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource OnAccent}"/>
</Style>
<Style Selector="Button.primary:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentPressed}"/>
</Style>
<Style Selector="Button.primary:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource Muted}"/>
</Style>
<Style Selector="Button.ghost">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource Accent}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Accent}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="16,9"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentSoft}"/>
</Style>
<!-- ============ Поля ввода ============ -->
<Style Selector="TextBox">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="Background" Value="{DynamicResource InputBg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Border}"/>
<Setter Property="MinHeight" Value="38"/>
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="BorderBrush" Value="{DynamicResource Accent}"/>
</Style>
<Style Selector="ComboBox">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="MinHeight" Value="38"/>
<Setter Property="Padding" Value="10,6"/>
<Setter Property="Background" Value="{DynamicResource InputBg}"/>
<Setter Property="BorderBrush" Value="{DynamicResource Border}"/>
</Style>
<Style Selector="DatePicker">
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="MinHeight" Value="38"/>
</Style>
<!-- ============ Сайдбар-навигация ============ -->
<Style Selector="ListBox.nav">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style Selector="ListBox.nav ListBoxItem">
<Setter Property="Foreground" Value="{DynamicResource SidebarText}"/>
<Setter Property="Padding" Value="14,11"/>
<Setter Property="Margin" Value="10,2"/>
<Setter Property="CornerRadius" Value="9"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="ListBox.nav ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
</Style>
<Style Selector="ListBox.nav ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
</Style>
<Style Selector="ListBox.nav ListBoxItem:selected">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<!-- ============ Верхняя навигация (как в WinForms-версии) ============ -->
<Style Selector="ListBox.topnav">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style Selector="ListBox.topnav ListBoxItem">
<Setter Property="Padding" Value="6,7"/>
<Setter Property="Margin" Value="2,0"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="ListBox.topnav ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
</Style>
<Style Selector="ListBox.topnav ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
</Style>
<!-- ============ Вкладки-документы ============ -->
<Style Selector="ListBox.tabs">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem">
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="3,6"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Foreground" Value="{DynamicResource Muted}"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource RowAlt}"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentSoft}"/>
</Style>
<Style Selector="ListBox.tabs ListBoxItem:selected">
<Setter Property="Foreground" Value="{DynamicResource AccentPressed}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<!-- ============ Таблица ============ -->
<Style Selector="DataGrid">
<Setter Property="Background" Value="{DynamicResource Surface}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource Border}"/>
<Setter Property="RowBackground" Value="{DynamicResource Surface}"/>
<Setter Property="Foreground" Value="{DynamicResource Text}"/>
<Setter Property="RowHeight" Value="34"/>
<Setter Property="FontSize" Value="13"/>
</Style>
<Style Selector="DataGridColumnHeader">
<Setter Property="Background" Value="{DynamicResource Accent}"/>
<Setter Property="Foreground" Value="{DynamicResource OnAccent}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="10,8"/>
<Setter Property="MinHeight" Value="36"/>
<Setter Property="SeparatorBrush" Value="#2FFFFFFF"/>
</Style>
<Style Selector="DataGridCell">
<Setter Property="Padding" Value="10,0"/>
</Style>
<!-- числовые ячейки — вправо -->
<Style Selector="DataGridCell.num TextBlock">
<Setter Property="TextAlignment" Value="Right"/>
</Style>
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource AccentSoft}"/>
<Setter Property="Opacity" Value="1"/>
</Style>
</Styles>

View File

@ -1,37 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Elfisa.Avalonia.ViewModels;
namespace Elfisa.Avalonia;
/// <summary>
/// Given a view model, returns the corresponding view if possible.
/// </summary>
[RequiresUnreferencedCode(
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
public class ViewLocator : IDataTemplate
{
public Control? Build(object? param)
{
if (param is null)
return null;
var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}

View File

@ -1,71 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public partial class CartItem : ObservableObject
{
public PriceItem Source { get; }
public string Name => Source.DrugName ?? "";
public string Supplier => Source.SupplierName ?? "";
public decimal Price => Source.Price;
[ObservableProperty] private decimal _qty = 1;
public decimal LineSum => Price * Qty;
public event Action? Changed;
public CartItem(PriceItem src) => Source = src;
partial void OnQtyChanged(decimal value)
{
OnPropertyChanged(nameof(LineSum));
Changed?.Invoke();
}
}
/// <summary>Единая корзина заказа — общая для «Прайс-листа» и «Отправить».</summary>
public sealed class CartService
{
public static CartService Instance { get; } = new();
public ObservableCollection<CartItem> Items { get; } = new();
public event Action? Changed;
public decimal Total => Items.Sum(c => c.LineSum);
public int Count => Items.Count;
public void Add(PriceItem src, decimal qty = 1)
{
if (src is null || string.IsNullOrEmpty(src.SupplierPriceId)) return;
var existing = Items.FirstOrDefault(c => c.Source.SupplierPriceId == src.SupplierPriceId);
if (existing != null) existing.Qty += qty;
else
{
var ci = new CartItem(src) { Qty = qty };
ci.Changed += Raise;
Items.Add(ci);
}
Raise();
}
public void Remove(CartItem item)
{
if (item is null) return;
item.Changed -= Raise;
Items.Remove(item);
Raise();
}
public void Clear()
{
foreach (var i in Items) i.Changed -= Raise;
Items.Clear();
Raise();
}
private void Raise() => Changed?.Invoke();
}

View File

@ -1,48 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
/// <summary>Справочник покупателя: профиль + грузополучатели (адреса).</summary>
public partial class DirectoryViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
public string UserName => Session.Current.Username ?? "";
public string RoleLabel => Session.Current.Role switch
{
"buyer" => "Покупатель",
"supplier" => "Поставщик",
"manager" => "Менеджер",
"admin" => "Администратор",
_ => Session.Current.Role ?? ""
};
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
[ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status;
public DirectoryViewModel()
{
_api.SetToken(Session.Current.Token);
if (Session.Current.IsAuthenticated) _ = LoadAsync();
}
private async Task LoadAsync()
{
try
{
Busy = true;
var locs = await _api.GetBuyerLocationsAsync();
Locations.Clear();
foreach (var l in locs) Locations.Add(l);
if (Locations.Count == 0) Status = "Грузополучатели не заданы.";
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
}

View File

@ -1,72 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public sealed class InvoiceRow
{
public string Date { get; init; } = "";
public string Number { get; init; } = "";
public string Supplier { get; init; } = "";
public string Location { get; init; } = "";
public int ItemsCount { get; init; }
public decimal Sum { get; init; }
}
/// <summary>
/// Накладные — реестр приходных документов. Строится из /api/buyer/report
/// (после деплоя фикса /api/buyer/orders можно перейти на «родной» источник).
/// </summary>
public partial class InvoicesViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
public ObservableCollection<InvoiceRow> Rows { get; } = new();
[ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status;
[ObservableProperty] private string _totals = "";
public InvoicesViewModel()
{
_api.SetToken(Session.Current.Token);
if (Session.Current.IsAuthenticated) _ = LoadAsync();
}
private async Task LoadAsync()
{
try
{
Busy = true;
var to = DateTime.Today;
var from = to.AddMonths(-24);
var lines = await _api.GetBuyerReportAsync(from, to);
Rows.Clear();
foreach (var g in lines.GroupBy(l => l.OrderId)
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
{
var f = g.First();
Rows.Add(new InvoiceRow
{
Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
Number = f.GlobalSign ?? "",
Supplier = string.Join(", ", g.Select(x => x.Supplier)
.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
Location = f.Location ?? "",
ItemsCount = g.Count(),
Sum = g.Sum(x => x.Sum)
});
}
Totals = $"Накладных: {Rows.Count} Сумма: {Rows.Sum(r => r.Sum):N2}";
if (Rows.Count == 0) Status = "Накладных за последние 2 года нет.";
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
}

View File

@ -1,100 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
/// <summary>«Загрузить» — скачивание подключённых прайсов с сервера в память.</summary>
public partial class LoadPriceViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
/// <summary>Прайсы, подключённые покупателю (какие именно скачиваем).</summary>
public ObservableCollection<BuyerPriceListDto> PriceLists { get; } = new();
[ObservableProperty] private bool _busy;
[ObservableProperty] private double _progress; // 0..100
[ObservableProperty] private string _progressText = "";
[ObservableProperty] private string? _result;
[ObservableProperty] private string _priceListsInfo = "Загрузка списка прайсов…";
public string LastLoaded =>
PriceCache.LoadedAt is { } dt
? $"Последняя загрузка: {dt:dd.MM.yyyy HH:mm} — {PriceCache.Items.Count} позиций"
: "Прайс ещё не загружался в этой сессии.";
public LoadPriceViewModel()
{
_api.SetToken(Session.Current.Token);
if (Session.Current.IsAuthenticated)
_ = LoadPriceListsAsync();
}
/// <summary>Тянет список подключённых прайсов — чтобы было видно, что именно скачиваем.</summary>
private async Task LoadPriceListsAsync()
{
try
{
var lists = await _api.GetBuyerPriceListsAsync();
PriceLists.Clear();
foreach (var pl in lists) PriceLists.Add(pl);
PriceListsInfo = lists.Count == 0
? "Прайсы пока не подключены — обратитесь к менеджеру."
: $"Подключено прайсов: {lists.Count}";
}
catch { PriceListsInfo = "Не удалось получить список подключённых прайсов."; }
}
[RelayCommand]
private async Task LoadAsync()
{
Result = null;
try
{
Busy = true;
Progress = 0;
ProgressText = "Соединение с сервером…";
var all = new List<PriceItem>();
const int page = 500;
int offset = 0, total = 0;
while (true)
{
var resp = await _api.GetPriceSummaryAsync(null, limit: page, offset: offset);
var batch = resp.Summary ?? new();
if (resp.TotalDrugs > 0) total = resp.TotalDrugs;
all.AddRange(batch);
Progress = total > 0 ? Math.Min(100.0, all.Count * 100.0 / total) : 0;
ProgressText = total > 0
? $"Загружено {all.Count} из {total} позиций…"
: $"Загружено {all.Count} позиций…";
offset += batch.Count;
if (batch.Count < page || (total > 0 && all.Count >= total) || offset > 30000)
break;
}
PriceCache.Items = all;
PriceCache.LoadedAt = DateTime.Now;
Progress = 100;
ProgressText = "Готово.";
var suppliers = all.Select(x => x.SupplierName).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct().Count();
Result = $"Прайс загружен: {all.Count} позиций от {suppliers} поставщик(ов).";
OnPropertyChanged(nameof(LastLoaded));
}
catch (Exception ex)
{
Result = "Ошибка загрузки: " + ex.Message;
ProgressText = "";
}
finally { Busy = false; }
}
}

View File

@ -1,44 +0,0 @@
using System;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public partial class LoginViewModel : ViewModelBase
{
private readonly Action _onSuccess;
private readonly ApiClient _api = new();
[ObservableProperty] private string _username = "";
[ObservableProperty] private string _password = "";
[ObservableProperty] private string? _error;
[ObservableProperty] private bool _busy;
public LoginViewModel(Action onSuccess) => _onSuccess = onSuccess;
[RelayCommand]
private async Task LoginAsync()
{
Error = null;
if (string.IsNullOrWhiteSpace(Username) || string.IsNullOrWhiteSpace(Password))
{
Error = "Введите логин и пароль.";
return;
}
try
{
Busy = true;
var resp = await _api.LoginAsync(Username.Trim(), Password);
Session.Current.Token = resp.Token;
Session.Current.Role = resp.Role;
Session.Current.Username = Username.Trim();
_onSuccess();
}
catch (ApiException ex) { Error = ex.Message; }
catch (Exception ex) { Error = "Не удалось войти: " + ex.Message; }
finally { Busy = false; }
}
}

View File

@ -1,165 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public sealed class OrderItemRow
{
public string Name { get; init; } = "";
public string Code { get; init; } = "";
public decimal Qty { get; init; }
public decimal Price { get; init; }
public decimal Sum { get; init; }
public string? SupplierPriceId { get; init; }
public string? Barcode { get; init; }
}
public sealed class OrderSummary
{
public string Date { get; init; } = "";
public string Number { get; init; } = "";
public string Supplier { get; init; } = "";
public string Location { get; init; } = "";
public string Status { get; init; } = "";
public int ItemsCount { get; init; }
public decimal Sum { get; init; }
public ObservableCollection<OrderItemRow> Items { get; init; } = new();
public bool IsDraft { get; init; }
public string? DraftId { get; init; }
public string? LocationId { get; init; }
public string? Comment { get; init; }
}
/// <summary>
/// Заказы: локальные черновики (статус «Не отправлен») + серверные заказы (из /api/buyer/report).
/// Черновик можно отправить (создать реальный заказ) или удалить.
/// </summary>
public partial class OrdersViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
public ObservableCollection<OrderSummary> Orders { get; } = new();
[ObservableProperty] private OrderSummary? _selectedOrder;
[ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status;
public OrdersViewModel()
{
_api.SetToken(Session.Current.Token);
if (Session.Current.IsAuthenticated) _ = LoadAsync();
}
[RelayCommand]
private async Task LoadAsync()
{
Status = null;
try
{
Busy = true;
Orders.Clear();
// 1) Локальные черновики — вверху, статус «Не отправлен»
foreach (var d in DraftStore.All)
{
var items = new ObservableCollection<OrderItemRow>(
d.Items.Select(i => new OrderItemRow
{
Name = i.Name, Qty = i.Qty, Price = i.Price, Sum = i.LineSum,
SupplierPriceId = i.SupplierPriceId, Barcode = i.Barcode
}));
Orders.Add(new OrderSummary
{
Date = d.CreatedAt.ToString("dd.MM.yyyy"),
Number = d.Number,
Supplier = string.Join(", ", d.Items.Select(i => i.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
Location = d.LocationAddress ?? "",
Status = "Не отправлен",
ItemsCount = d.ItemsCount,
Sum = d.Total,
Items = items,
IsDraft = true,
DraftId = d.Id,
LocationId = d.LocationId,
Comment = d.Comment
});
}
// 2) Серверные заказы
var to = DateTime.Today;
var from = to.AddMonths(-24);
var lines = await _api.GetBuyerReportAsync(from, to);
foreach (var g in lines.GroupBy(l => l.OrderId)
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
{
var f = g.First();
var items = new ObservableCollection<OrderItemRow>(
g.Select(l => new OrderItemRow
{
Name = l.ItemName ?? "", Code = l.ItemCode ?? "",
Qty = l.Qty, Price = l.UnitPrice, Sum = l.Sum
}));
Orders.Add(new OrderSummary
{
Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
Number = f.GlobalSign ?? "",
Supplier = string.Join(", ", g.Select(x => x.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
Location = f.Location ?? "",
Status = f.Status ?? "",
ItemsCount = g.Count(),
Sum = g.Sum(x => x.Sum),
Items = items
});
}
SelectedOrder = Orders.FirstOrDefault();
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
[RelayCommand]
private async Task SendDraftAsync()
{
if (SelectedOrder is not { IsDraft: true } d) return;
Status = null;
try
{
Busy = true;
var req = new BuyerOrderCreateRequest
{
GlobalSign = d.Number.StartsWith("EX-", StringComparison.OrdinalIgnoreCase) ? d.Number : null,
LocationId = d.LocationId,
Comment = d.Comment,
Items = d.Items.Select(i => new BuyerOrderItemReq
{
SupplierPriceId = i.SupplierPriceId ?? "",
Qty = (double)i.Qty,
ItemName = i.Name,
Barcode = i.Barcode
}).ToList()
};
var resp = await _api.CreateOrderAsync(req);
if (d.DraftId is { } id) DraftStore.Remove(id);
Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} отправлен ({resp.ItemsCount} поз., {resp.TotalAmount:N2}).";
await LoadAsync();
}
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
finally { Busy = false; }
}
[RelayCommand]
private async Task DeleteDraftAsync()
{
if (SelectedOrder is not { IsDraft: true } d || d.DraftId is not { } id) return;
DraftStore.Remove(id);
await LoadAsync();
Status = "Черновик удалён.";
}
}

View File

@ -1,15 +0,0 @@
namespace Elfisa.Avalonia.ViewModels;
/// <summary>Заглушка ещё не портированного раздела.</summary>
public partial class PlaceholderPageViewModel : ViewModelBase
{
public string Title { get; }
public string Description { get; }
public string Note { get; } = "Раздел портируется на Avalonia. Пока доступен в WinForms-клиенте.";
public PlaceholderPageViewModel(string title, string description)
{
Title = title;
Description = description;
}
}

View File

@ -1,281 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
/// <summary>Строка предложения с редактируемым количеством «Заказ» (как колонка «Заказ» в WinForms).</summary>
public partial class OfferRow : ObservableObject
{
public PriceItem Source { get; }
public string? DrugName => Source.DrugName;
public string? SupplierName => Source.SupplierName;
public decimal Price => Source.Price;
public decimal Quantity => Source.Quantity;
[ObservableProperty] private decimal _orderQty = 1;
public OfferRow(PriceItem src) => Source = src;
}
/// <summary>
/// Прайс-лист + сборка заказа. Каталог — как в старой программе:
/// база (название) → дозировка (МГ) → предложения поставщиков. Справа «Мой заказ».
/// «Сохранить заказ» кладёт черновик в «Заказы» (статус «Не отправлен»).
/// </summary>
public partial class PriceListViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
private readonly CartService _cart = CartService.Instance;
// Плоский список предложений + предвычисленные база/МГ (как «Базовое наименование»/«МГ» в WinForms).
private readonly List<(PriceItem Item, string BaseName, string Mg)> _indexed = new();
private const string EmptyMgDisplay = "(без МГ)";
private static readonly Regex DosageRegex = new(
@"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// Три панели дрилдауна
public ObservableCollection<string> BaseNames { get; } = new();
public ObservableCollection<string> MgOptions { get; } = new();
public ObservableCollection<OfferRow> Offers { get; } = new();
public ObservableCollection<CartItem> Cart => _cart.Items;
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
public ObservableCollection<BuyerPriceListDto> MyPriceLists { get; } = new();
[ObservableProperty] private string _search = "";
[ObservableProperty] private BuyerPriceListDto? _selectedPriceList;
[ObservableProperty] private bool _hasMultiplePriceLists;
[ObservableProperty] private string? _selectedBaseName;
[ObservableProperty] private string? _selectedMg;
[ObservableProperty] private OfferRow? _selectedOffer;
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
[ObservableProperty] private string _comment = "";
[ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status;
[ObservableProperty] private int _total;
[ObservableProperty] private decimal _cartTotal;
public PriceListViewModel()
{
_api.SetToken(Session.Current.Token);
_cart.Changed += Recalc;
Recalc();
if (Session.Current.IsAuthenticated)
{
_ = LoadPriceListsAsync(); // грузит «Мои прайсы» → ставит выбор → запускает поиск
_ = LoadLocationsAsync();
}
}
private bool _suppressPriceListSearch;
/// <summary>Загружает подключённые покупателю прайсы в дропдаун «Мои прайсы».</summary>
private async Task LoadPriceListsAsync()
{
List<BuyerPriceListDto> lists;
try { lists = await _api.GetBuyerPriceListsAsync(); }
catch { lists = new(); }
MyPriceLists.Clear();
MyPriceLists.Add(new BuyerPriceListDto { PriceListId = null, Name = "Все прайсы" });
foreach (var pl in lists) MyPriceLists.Add(pl);
HasMultiplePriceLists = lists.Count > 1;
_suppressPriceListSearch = true;
// Один подключён — выбираем его; несколько или ни одного — «Все прайсы».
SelectedPriceList = lists.Count == 1 ? MyPriceLists[1] : MyPriceLists[0];
_suppressPriceListSearch = false;
await SearchAsync();
}
partial void OnSelectedPriceListChanged(BuyerPriceListDto? value)
{
if (_suppressPriceListSearch) return;
_ = SearchAsync();
}
private void Recalc() => CartTotal = _cart.Total;
private async Task LoadLocationsAsync()
{
try
{
var locs = await _api.GetBuyerLocationsAsync();
Locations.Clear();
foreach (var l in locs) Locations.Add(l);
var savedId = SettingsStore.Current.DefaultLocationId;
SelectedLocation = Locations.FirstOrDefault(l => l.BuyerLocationId == savedId)
?? Locations.FirstOrDefault(l => l.IsDefault)
?? Locations.FirstOrDefault();
}
catch { }
}
[RelayCommand]
private async Task SearchAsync()
{
Status = null;
try
{
Busy = true;
var resp = await _api.GetPriceSummaryAsync(Search, limit: 300, priceListId: SelectedPriceList?.PriceListId);
RebuildIndex(resp.Summary ?? new());
Total = resp.TotalDrugs;
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
/// <summary>Пересобирает индекс база/МГ и список базовых наименований.</summary>
private void RebuildIndex(List<PriceItem> items)
{
_indexed.Clear();
foreach (var it in items)
{
var mg = ExtractDosage(it.DrugName);
var baseName = BuildBaseName(it.DrugName, it.TradeName, mg);
if (string.IsNullOrWhiteSpace(baseName)) continue;
_indexed.Add((it, baseName, mg));
}
// Сброс выбора и панелей
SelectedBaseName = null;
MgOptions.Clear();
Offers.Clear();
SelectedOffer = null;
var names = _indexed
.Select(x => x.BaseName)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
.ToList();
BaseNames.Clear();
foreach (var n in names) BaseNames.Add(n);
}
// База выбрана → заполняем список дозировок.
partial void OnSelectedBaseNameChanged(string? value)
{
MgOptions.Clear();
Offers.Clear();
SelectedOffer = null;
SelectedMg = null;
if (string.IsNullOrEmpty(value)) return;
var mgs = _indexed
.Where(x => string.Equals(x.BaseName, value, StringComparison.OrdinalIgnoreCase))
.Select(x => string.IsNullOrEmpty(x.Mg) ? EmptyMgDisplay : x.Mg)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(m => m, StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var m in mgs) MgOptions.Add(m);
// Если дозировка одна — сразу её выбираем (меньше кликов).
if (MgOptions.Count == 1) SelectedMg = MgOptions[0];
}
// Дозировка выбрана → показываем предложения (фильтр база + МГ).
partial void OnSelectedMgChanged(string? value)
{
Offers.Clear();
SelectedOffer = null;
if (string.IsNullOrEmpty(SelectedBaseName) || string.IsNullOrEmpty(value)) return;
var wantMg = string.Equals(value, EmptyMgDisplay, StringComparison.Ordinal) ? "" : value;
var offers = _indexed
.Where(x => string.Equals(x.BaseName, SelectedBaseName, StringComparison.OrdinalIgnoreCase)
&& string.Equals(x.Mg, wantMg, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.Item.Price)
.Select(x => new OfferRow(x.Item))
.ToList();
foreach (var o in offers) Offers.Add(o);
SelectedOffer = Offers.FirstOrDefault();
}
private static string ExtractDosage(string? drugName)
{
if (string.IsNullOrWhiteSpace(drugName)) return string.Empty;
var m = DosageRegex.Match(drugName);
return m.Success ? m.Value.Trim() : string.Empty;
}
private static string BuildBaseName(string? drugName, string? tradeName, string dosage)
{
if (!string.IsNullOrWhiteSpace(tradeName)) return tradeName.Trim();
var name = drugName ?? string.Empty;
if (!string.IsNullOrWhiteSpace(dosage))
{
var idx = name.IndexOf(dosage, StringComparison.OrdinalIgnoreCase);
if (idx >= 0) name = name.Remove(idx, dosage.Length);
}
name = Regex.Replace(name, @"\s{2,}", " ").Trim(' ', ',', '-', '.', ';');
return string.IsNullOrWhiteSpace(name) ? (drugName ?? string.Empty).Trim() : name;
}
[RelayCommand]
private void AddToOrder() => AddSelectedOffer();
/// <summary>Добавляет выбранное предложение в заказ с его количеством (Enter в таблице / кнопка).</summary>
public void AddSelectedOffer()
{
if (SelectedOffer is null) { Status = "Выберите предложение в списке."; return; }
var q = SelectedOffer.OrderQty <= 0 ? 1 : SelectedOffer.OrderQty;
_cart.Add(SelectedOffer.Source, q);
SelectedOffer.OrderQty = 1;
Status = null;
}
[RelayCommand]
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
[RelayCommand]
private async Task SaveDraftAsync()
{
if (_cart.Count == 0) { Status = "Заказ пуст — добавьте позиции."; return; }
try
{
Busy = true;
// Резервируем настоящий номер заказа EX-####### (как в WinForms)
var sign = await _api.NextGlobalSignAsync();
var draft = new DraftOrder
{
Number = string.IsNullOrWhiteSpace(sign) ? $"ЧРН-{DateTime.Now:HHmmss}" : sign!,
LocationId = SelectedLocation?.BuyerLocationId,
LocationAddress = SelectedLocation?.Address,
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
Items = _cart.Items.Select(c => new DraftItem
{
SupplierPriceId = c.Source.SupplierPriceId ?? "",
Name = c.Name,
Supplier = c.Supplier,
Barcode = c.Source.Barcode,
Price = c.Price,
Qty = c.Qty
}).ToList()
};
DraftStore.Add(draft);
_cart.Clear();
Comment = "";
Status = $"Заказ сохранён: {draft.Number} → раздел «Заказы» (не отправлен).";
}
catch (Exception ex) { Status = "Ошибка сохранения: " + ex.Message; }
finally { Busy = false; }
}
}

View File

@ -1,215 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public sealed class ReportColumn
{
public string Header { get; init; } = "";
public bool Numeric { get; init; }
public double Weight { get; init; } = 100; // ширина в пикселях (для нетянущихся)
public bool Stretch { get; init; } // тянуть на всё свободное место (star)
}
/// <summary>Строка отчёта: ячейки как строки, доступ по индексу для DataGrid.</summary>
public sealed class ReportRow
{
private readonly string[] _cells;
public ReportRow(string[] cells) => _cells = cells;
public string this[int i] => i >= 0 && i < _cells.Length ? _cells[i] : "";
}
public partial class ReportsViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
private List<BuyerReportLineDto> _lines = new();
/// <summary>Сигнал View пересобрать колонки DataGrid.</summary>
public event Action? ColumnsChanged;
public ObservableCollection<ReportColumn> Columns { get; } = new();
public ObservableCollection<ReportRow> Rows { get; } = new();
[ObservableProperty] private DateTimeOffset _dateFrom = DateTimeOffset.Now.AddMonths(-1);
[ObservableProperty] private DateTimeOffset _dateTo = DateTimeOffset.Now;
public string[] ReportTypes { get; } = { "Мои заказы", "По поставщикам", "По товарам", "По аптекам" };
[ObservableProperty] private int _selectedReportIndex;
public string[] AiFormats { get; } = { "Word", "Excel" };
[ObservableProperty] private int _selectedAiFormat;
[ObservableProperty] private string _aiPrompt = "";
[ObservableProperty] private string _totals = "";
[ObservableProperty] private string? _status;
[ObservableProperty] private bool _busy;
[ObservableProperty] private bool _aiBusy;
public ReportsViewModel()
{
_api.SetToken(Session.Current.Token);
if (Session.Current.IsAuthenticated)
_ = BuildAsync(); // авто-загрузка за последний месяц при открытии
}
partial void OnSelectedReportIndexChanged(int value) => Render();
[RelayCommand]
private async Task BuildAsync()
{
Status = null;
try
{
Busy = true;
_lines = await _api.GetBuyerReportAsync(DateFrom.DateTime.Date, DateTo.DateTime.Date);
Render();
if (_lines.Count == 0) Status = "За выбранный период заказов нет.";
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
[RelayCommand]
private async Task GenerateAiAsync()
{
Status = null;
if (string.IsNullOrWhiteSpace(AiPrompt)) { Status = "Введите запрос для ИИ."; return; }
try
{
AiBusy = true;
var fmt = SelectedAiFormat == 1 ? "xlsx" : "docx";
var res = await _api.GenerateAiReportAsync(AiPrompt.Trim(), fmt);
var dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var safe = string.Join("_", res.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, res.Content);
Status = "Отчёт сохранён: " + path;
OpenFile(path);
}
catch (Exception ex) { Status = ex.Message; }
finally { AiBusy = false; }
}
private void Render()
{
Columns.Clear();
Rows.Clear();
var (cols, rows) = Aggregate(_lines, SelectedReportIndex);
foreach (var c in cols) Columns.Add(c);
foreach (var r in rows) Rows.Add(new ReportRow(r));
var orders = _lines.Select(l => l.OrderId).Distinct().Count();
var sum = _lines.Sum(l => l.Sum);
Totals = $"Заказов: {orders} Позиций: {_lines.Count} Сумма: {sum:N2}";
ColumnsChanged?.Invoke();
}
private static (List<ReportColumn> cols, List<string[]> rows) Aggregate(List<BuyerReportLineDto> lines, int type)
{
string N2(decimal d) => d.ToString("N2");
string N3(decimal d) => d.ToString("N3");
string D(DateTime? dt) => dt?.ToLocalTime().ToString("dd.MM.yyyy") ?? "";
switch (type)
{
case 1: // По поставщикам
{
var cols = new List<ReportColumn>
{
new() { Header = "Поставщик", Stretch = true },
new() { Header = "Заказов", Numeric = true, Weight = 90 },
new() { Header = "Позиций", Numeric = true, Weight = 90 },
new() { Header = "Сумма", Numeric = true, Weight = 120 },
};
var rows = lines.GroupBy(l => string.IsNullOrWhiteSpace(l.Supplier) ? "(не указан)" : l.Supplier!)
.OrderByDescending(g => g.Sum(x => x.Sum))
.Select(g => new[] { g.Key, g.Select(x => x.OrderId).Distinct().Count().ToString(),
g.Count().ToString(), N2(g.Sum(x => x.Sum)) }).ToList();
return (cols, rows);
}
case 2: // По товарам
{
var cols = new List<ReportColumn>
{
new() { Header = "Товар", Stretch = true },
new() { Header = "Код", Weight = 100 },
new() { Header = "Кол-во", Numeric = true, Weight = 100 },
new() { Header = "Сумма", Numeric = true, Weight = 120 },
};
var rows = lines.GroupBy(l => ((l.ItemName ?? "").Trim() + "|" + (l.ItemCode ?? "")))
.OrderByDescending(g => g.Sum(x => x.Sum))
.Select(g => { var f = g.First(); return new[] { f.ItemName ?? "", f.ItemCode ?? "",
N3(g.Sum(x => x.Qty)), N2(g.Sum(x => x.Sum)) }; }).ToList();
return (cols, rows);
}
case 3: // По аптекам
{
var cols = new List<ReportColumn>
{
new() { Header = "Аптека", Stretch = true },
new() { Header = "Заказов", Numeric = true, Weight = 90 },
new() { Header = "Позиций", Numeric = true, Weight = 90 },
new() { Header = "Сумма", Numeric = true, Weight = 120 },
};
var rows = lines.GroupBy(l => string.IsNullOrWhiteSpace(l.Location) ? "(не указана)" : l.Location!)
.OrderByDescending(g => g.Sum(x => x.Sum))
.Select(g => new[] { g.Key, g.Select(x => x.OrderId).Distinct().Count().ToString(),
g.Count().ToString(), N2(g.Sum(x => x.Sum)) }).ToList();
return (cols, rows);
}
default: // Мои заказы
{
var cols = new List<ReportColumn>
{
new() { Header = "Дата", Weight = 105 },
new() { Header = "Номер", Weight = 138 },
new() { Header = "Поставщик", Weight = 150 },
new() { Header = "Аптека", Stretch = true },
new() { Header = "Позиций", Numeric = true, Weight = 95 },
new() { Header = "Сумма", Numeric = true, Weight = 125 },
new() { Header = "Статус", Weight = 100 },
};
var rows = lines.GroupBy(l => l.OrderId)
.OrderByDescending(g => g.First().OrderDate ?? DateTime.MinValue)
.Select(g =>
{
var f = g.First();
var sup = string.Join(", ", g.Select(x => x.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct());
return new[] { D(f.OrderDate), f.GlobalSign ?? "", sup, f.Location ?? "",
g.Count().ToString(), N2(g.Sum(x => x.Sum)), f.Status ?? "" };
}).ToList();
return (cols, rows);
}
}
}
private static void OpenFile(string path)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true });
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
Process.Start("open", $"\"{path}\"");
else
Process.Start("xdg-open", $"\"{path}\"");
}
catch { /* открыть не удалось — путь показан в статусе */ }
}
}

View File

@ -1,48 +0,0 @@
using System;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
/// <summary>Корневой VM: переключает экран входа и основной шелл.</summary>
public partial class RootViewModel : ViewModelBase
{
private ViewModelBase _current = null!;
public ViewModelBase Current
{
get => _current;
private set => SetProperty(ref _current, value);
}
public RootViewModel()
{
Current = new LoginViewModel(OnLoggedIn);
TryDevAutoLogin();
}
// Dev-режим: если заданы ELF_DEV_USER/ELF_DEV_PASS — входим автоматически.
// Обычных пользователей не затрагивает (переменных нет).
private async void TryDevAutoLogin()
{
var u = Environment.GetEnvironmentVariable("ELF_DEV_USER");
var p = Environment.GetEnvironmentVariable("ELF_DEV_PASS");
if (string.IsNullOrWhiteSpace(u) || string.IsNullOrWhiteSpace(p)) return;
try
{
var api = new ApiClient();
var resp = await api.LoginAsync(u, p);
Session.Current.Token = resp.Token;
Session.Current.Role = resp.Role;
Session.Current.Username = u;
OnLoggedIn();
}
catch { /* остаёмся на экране входа */ }
}
private void OnLoggedIn() => Current = new ShellViewModel(Logout);
private void Logout()
{
Session.Current.Clear();
Current = new LoginViewModel(OnLoggedIn);
}
}

View File

@ -1,106 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
/// <summary>
/// Отправить: поиск по прайсу → корзина (общая с Прайс-листом) → создание заказа.
/// ВНИМАНИЕ: отправка создаёт реальный заказ у поставщика.
/// </summary>
public partial class SendOrderViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
private readonly CartService _cart = CartService.Instance;
public ObservableCollection<PriceItem> Results { get; } = new();
public ObservableCollection<CartItem> Cart => _cart.Items;
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
[ObservableProperty] private string _search = "";
[ObservableProperty] private PriceItem? _selectedResult;
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
[ObservableProperty] private string _comment = "";
[ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status;
[ObservableProperty] private decimal _cartTotal;
public SendOrderViewModel()
{
_api.SetToken(Session.Current.Token);
_cart.Changed += Recalc;
Recalc();
if (Session.Current.IsAuthenticated) _ = InitAsync();
}
private async Task InitAsync()
{
try
{
var locs = await _api.GetBuyerLocationsAsync();
Locations.Clear();
foreach (var l in locs) Locations.Add(l);
SelectedLocation = Locations.FirstOrDefault(l => l.IsDefault) ?? Locations.FirstOrDefault();
await SearchAsync();
}
catch (Exception ex) { Status = ex.Message; }
}
[RelayCommand]
private async Task SearchAsync()
{
try
{
Busy = true; Status = null;
var resp = await _api.GetPriceSummaryAsync(Search, limit: 200);
Results.Clear();
foreach (var it in resp.Summary ?? new()) Results.Add(it);
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
[RelayCommand]
private void AddToCart()
{
if (SelectedResult is null) { Status = "Выберите позицию в списке слева."; return; }
_cart.Add(SelectedResult);
}
[RelayCommand]
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
private void Recalc() => CartTotal = _cart.Total;
[RelayCommand]
private async Task SendAsync()
{
Status = null;
if (_cart.Count == 0) { Status = "Корзина пуста."; return; }
try
{
Busy = true;
var req = new BuyerOrderCreateRequest
{
LocationId = SelectedLocation?.BuyerLocationId,
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
Items = _cart.Items.Select(c => new BuyerOrderItemReq
{
SupplierPriceId = c.Source.SupplierPriceId ?? "",
Qty = (double)c.Qty,
ItemName = c.Source.DrugName,
Barcode = c.Source.Barcode
}).ToList()
};
var resp = await _api.CreateOrderAsync(req);
Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} создан: {resp.ItemsCount} поз., {resp.TotalAmount:N2}.";
_cart.Clear();
}
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
finally { Busy = false; }
}
}

View File

@ -1,43 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public partial class SettingsViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
[ObservableProperty] private string _apiBaseUrl = "";
public string[] Themes { get; } = { "Светлая", "Тёмная" };
[ObservableProperty] private int _themeIndex;
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
public SettingsViewModel()
{
_api.SetToken(Session.Current.Token);
ApiBaseUrl = SettingsStore.Current.ApiBaseUrl ?? AppConfig.DefaultApiBaseUrl;
ThemeIndex = SettingsStore.Current.Theme == "Dark" ? 1 : 0;
if (Session.Current.IsAuthenticated) _ = LoadLocationsAsync();
}
private async Task LoadLocationsAsync()
{
try
{
var locs = await _api.GetBuyerLocationsAsync();
Locations.Clear();
foreach (var l in locs) Locations.Add(l);
var savedId = SettingsStore.Current.DefaultLocationId;
SelectedLocation = Locations.FirstOrDefault(l => l.BuyerLocationId == savedId)
?? Locations.FirstOrDefault(l => l.IsDefault)
?? Locations.FirstOrDefault();
}
catch { /* локации не критичны для настроек */ }
}
}

View File

@ -1,136 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public class NavItem
{
public string Title { get; }
public string Icon { get; } // глиф Segoe MDL2 Assets
public Func<ViewModelBase> Factory { get; }
public NavItem(string title, string icon, Func<ViewModelBase> factory)
{
Title = title; Icon = icon; Factory = factory;
}
}
/// <summary>Открытая вкладка-документ.</summary>
public sealed class DocumentTab
{
public string Title { get; }
public string Key { get; }
public ViewModelBase Content { get; }
public DocumentTab(string title, string key, ViewModelBase content)
{
Title = title; Key = key; Content = content;
}
}
public partial class ShellViewModel : ViewModelBase
{
private readonly Action _logout;
private readonly ApiClient _api = new();
private bool _sync;
public ObservableCollection<NavItem> NavItems { get; }
public ObservableCollection<DocumentTab> Tabs { get; } = new();
[ObservableProperty] private NavItem? _selectedNav;
[ObservableProperty] private DocumentTab? _activeTab;
public string UserName => Session.Current.Username ?? "";
public string RoleLabel => Session.Current.Role switch
{
"buyer" => "Покупатель",
"supplier" => "Поставщик",
"manager" => "Менеджер",
"admin" => "Администратор",
_ => Session.Current.Role ?? ""
};
[ObservableProperty] private string _pharmacyAddress = "";
// Segoe MDL2 Assets — те же глифы, что в WinForms-версии
private const string IcoPrice = "";
private const string IcoOrders = "";
private const string IcoInvoices = "";
private const string IcoReports = "";
private const string IcoUpload = "";
private const string IcoSend = "";
private const string IcoRefusals = "";
private const string IcoDirectory = "";
public ShellViewModel(Action logout)
{
_logout = logout;
_api.SetToken(Session.Current.Token);
NavItems = new ObservableCollection<NavItem>
{
new("Прайс-лист", IcoPrice, () => new PriceListViewModel()),
new("Заказы", IcoOrders, () => new OrdersViewModel()),
new("Накладные", IcoInvoices, () => new InvoicesViewModel()),
new("Отчёты", IcoReports, () => new ReportsViewModel()),
new("Загрузить", IcoUpload, () => new LoadPriceViewModel()),
new("Отправить", IcoSend, () => new SendOrderViewModel()),
new("Отказы", IcoRefusals, () => new PlaceholderPageViewModel("Отказы",
"Отказные позиции по заказам. Нужен серверный эндпоинт (RefuseItemsCount сейчас только в локальной БД WinForms).")),
new("Справочник", IcoDirectory, () => new DirectoryViewModel()),
};
SelectedNav = NavItems.First(n => n.Title == "Отчёты");
if (Session.Current.IsAuthenticated) _ = LoadPharmacyAsync();
}
private async Task LoadPharmacyAsync()
{
try
{
var locs = await _api.GetBuyerLocationsAsync();
var def = locs.FirstOrDefault(l => l.IsDefault) ?? locs.FirstOrDefault();
PharmacyAddress = def?.Address ?? "";
}
catch { }
}
partial void OnSelectedNavChanged(NavItem? value)
{
if (_sync || value is null) return;
OpenOrActivate(value);
}
private void OpenOrActivate(NavItem nav)
{
var existing = Tabs.FirstOrDefault(t => t.Key == nav.Title);
if (existing is not null) { ActiveTab = existing; return; }
var tab = new DocumentTab(nav.Title, nav.Title, nav.Factory());
Tabs.Add(tab);
ActiveTab = tab;
}
partial void OnActiveTabChanged(DocumentTab? value)
{
if (value is null) return;
_sync = true;
SelectedNav = NavItems.FirstOrDefault(n => n.Title == value.Key);
_sync = false;
}
[RelayCommand]
private void CloseTab(DocumentTab? tab)
{
if (tab is null) return;
int idx = Tabs.IndexOf(tab);
bool wasActive = ReferenceEquals(ActiveTab, tab);
Tabs.Remove(tab);
if (wasActive)
ActiveTab = Tabs.Count == 0 ? null : Tabs[Math.Min(idx, Tabs.Count - 1)];
}
[RelayCommand] private void Logout() => _logout();
}

View File

@ -1,7 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Elfisa.Avalonia.ViewModels;
public abstract class ViewModelBase : ObservableObject
{
}

View File

@ -1,56 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
xmlns:core="using:Elfisa.Core"
x:Class="Elfisa.Avalonia.Views.DirectoryView"
x:DataType="vm:DirectoryViewModel">
<ScrollViewer>
<StackPanel Margin="24" Spacing="14">
<StackPanel Spacing="2">
<TextBlock Text="Справочник" Classes="h1"/>
<TextBlock Text="Профиль и грузополучатели" Classes="muted"/>
</StackPanel>
<!-- Профиль -->
<Border Classes="card" MaxWidth="720" HorizontalAlignment="Left">
<StackPanel Spacing="10">
<TextBlock Text="Учётная запись" Classes="h2"/>
<Grid ColumnDefinitions="160,*" RowDefinitions="Auto,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="Логин" Classes="muted"/>
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding UserName}" Foreground="{DynamicResource Text}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Роль" Classes="muted" Margin="0,6,0,0"/>
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding RoleLabel}" Foreground="{DynamicResource Text}" Margin="0,6,0,0"/>
</Grid>
</StackPanel>
</Border>
<!-- Грузополучатели -->
<Border Classes="card" MaxWidth="720" HorizontalAlignment="Left">
<StackPanel Spacing="10">
<TextBlock Text="Грузополучатели (адреса)" Classes="h2"/>
<ItemsControl ItemsSource="{Binding Locations}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="core:BuyerLocationDto">
<Border Padding="12" Margin="0,0,0,8" CornerRadius="9"
Background="{DynamicResource RowAlt}" BorderThickness="1" BorderBrush="{DynamicResource Border}">
<StackPanel Spacing="3">
<TextBlock Text="{Binding Address}" Foreground="{DynamicResource Text}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="{Binding RegionName}" Classes="muted"/>
<Border Background="{DynamicResource AccentSoft}" CornerRadius="5" Padding="6,1"
IsVisible="{Binding IsDefault}">
<TextBlock Text="по умолчанию" FontSize="11" Foreground="{DynamicResource AccentPressed}"/>
</Border>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="{Binding Status}" Classes="muted"/>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@ -1,9 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Elfisa.Avalonia.Views;
public partial class DirectoryView : UserControl
{
public DirectoryView() => AvaloniaXamlLoader.Load(this);
}

View File

@ -1,39 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
xmlns:conv="using:Avalonia.Data.Converters"
x:Class="Elfisa.Avalonia.Views.InvoicesView"
x:DataType="vm:InvoicesViewModel">
<DockPanel Margin="24">
<Grid DockPanel.Dock="Top" Margin="0,0,0,14">
<StackPanel Spacing="2" VerticalAlignment="Center">
<TextBlock Text="Накладные" Classes="h1"/>
<TextBlock Text="Реестр приходных документов" Classes="muted"/>
</StackPanel>
<TextBlock Text="{Binding Totals}" HorizontalAlignment="Right" VerticalAlignment="Center"
FontWeight="SemiBold" Foreground="{DynamicResource Accent}"/>
</Grid>
<Border DockPanel.Dock="Top" Margin="0,0,0,10" Padding="10,7" CornerRadius="6"
Background="{DynamicResource AccentSoft}"
IsVisible="{Binding Status, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" FontSize="12.5"/>
</Border>
<Border Classes="card" Padding="0" ClipToBounds="True">
<DataGrid ItemsSource="{Binding Rows}" x:CompileBindings="False"
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
CanUserResizeColumns="True" CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Дата" Width="112" Binding="{Binding Date}"/>
<DataGridTextColumn Header="Номер" Width="150" Binding="{Binding Number}"/>
<DataGridTextColumn Header="Поставщик" Width="160" Binding="{Binding Supplier}"/>
<DataGridTextColumn Header="Аптека" Width="*" Binding="{Binding Location}"/>
<DataGridTextColumn Header="Позиций" Width="90" Binding="{Binding ItemsCount}" CellStyleClasses="num"/>
<DataGridTextColumn Header="Сумма" Width="132" Binding="{Binding Sum, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
</DataGrid.Columns>
</DataGrid>
</Border>
</DockPanel>
</UserControl>

View File

@ -1,9 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Elfisa.Avalonia.Views;
public partial class InvoicesView : UserControl
{
public InvoicesView() => AvaloniaXamlLoader.Load(this);
}

View File

@ -1,61 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
xmlns:conv="using:Avalonia.Data.Converters"
xmlns:core="using:Elfisa.Core"
x:Class="Elfisa.Avalonia.Views.LoadPriceView"
x:DataType="vm:LoadPriceViewModel">
<Grid Margin="24" RowDefinitions="Auto,*">
<StackPanel Grid.Row="0" Spacing="2">
<TextBlock Text="Загрузить" Classes="h1"/>
<TextBlock Text="Скачать актуальный прайс с сервера" Classes="muted"/>
</StackPanel>
<Border Grid.Row="1" Classes="card" MaxWidth="560" Padding="30"
HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Spacing="18" HorizontalAlignment="Center">
<Border Width="60" Height="60" CornerRadius="30" Background="{DynamicResource AccentSoft}">
<TextBlock Text="📥" FontSize="26" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding LastLoaded}" Classes="muted" HorizontalAlignment="Center" TextAlignment="Center"/>
<!-- Подключённые прайсы — что именно скачиваем -->
<Border Background="{DynamicResource RowAlt}" CornerRadius="10" Padding="16,12" Width="380"
BorderThickness="1" BorderBrush="{DynamicResource Border}">
<StackPanel Spacing="8">
<TextBlock Text="{Binding PriceListsInfo}" FontWeight="SemiBold" Foreground="{DynamicResource Text}"/>
<ItemsControl ItemsSource="{Binding PriceLists}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="core:BuyerPriceListDto">
<Grid ColumnDefinitions="Auto,*" Margin="0,3">
<TextBlock Grid.Column="0" Text="• " Foreground="{DynamicResource Accent}" FontWeight="Bold"/>
<StackPanel Grid.Column="1" Spacing="1">
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Text}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding SupplierName}" Classes="muted" FontSize="12"/>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<Button Classes="primary" Content="Загрузить прайс" HorizontalAlignment="Center"
Padding="28,12" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
<StackPanel Spacing="6" IsVisible="{Binding Busy}">
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" Height="8" Width="360"/>
<TextBlock Text="{Binding ProgressText}" Classes="muted" HorizontalAlignment="Center"/>
</StackPanel>
<Border Background="{DynamicResource AccentSoft}" CornerRadius="8" Padding="14,10"
IsVisible="{Binding Result, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
<TextBlock Text="{Binding Result}" Foreground="{DynamicResource AccentPressed}"
TextWrapping="Wrap" TextAlignment="Center"/>
</Border>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@ -1,9 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Elfisa.Avalonia.Views;
public partial class LoadPriceView : UserControl
{
public LoadPriceView() => AvaloniaXamlLoader.Load(this);
}

View File

@ -1,39 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
xmlns:conv="using:Avalonia.Data.Converters"
x:Class="Elfisa.Avalonia.Views.LoginView"
x:DataType="vm:LoginViewModel">
<Grid Background="{DynamicResource Bg}">
<Border Classes="card" Width="390" Padding="30"
HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Spacing="16">
<StackPanel Spacing="2" Margin="0,0,0,6">
<TextBlock Text="ЭльФиСА" FontSize="28" FontWeight="Bold"
Foreground="{DynamicResource Accent}" HorizontalAlignment="Center"/>
<TextBlock Text="Электронная Фармация" Classes="muted" HorizontalAlignment="Center"/>
</StackPanel>
<StackPanel Spacing="6">
<TextBlock Text="Логин" Classes="label"/>
<TextBox Text="{Binding Username}" Watermark="e-mail или логин"/>
</StackPanel>
<StackPanel Spacing="6">
<TextBlock Text="Пароль" Classes="label"/>
<TextBox Text="{Binding Password}" PasswordChar="●" Watermark="пароль"/>
</StackPanel>
<Border Background="#1FDC2626" CornerRadius="6" Padding="10,7"
IsVisible="{Binding Error, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
<TextBlock Text="{Binding Error}" Foreground="#DC2626" TextWrapping="Wrap" FontSize="12.5"/>
</Border>
<Button Classes="primary" Content="Войти" IsDefault="True"
HorizontalAlignment="Stretch" Margin="0,4,0,0"
Command="{Binding LoginCommand}" IsEnabled="{Binding !Busy}"/>
</StackPanel>
</Border>
</Grid>
</UserControl>

View File

@ -1,9 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Elfisa.Avalonia.Views;
public partial class LoginView : UserControl
{
public LoginView() => AvaloniaXamlLoader.Load(this);
}

View File

@ -1,16 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="740"
x:Class="Elfisa.Avalonia.Views.MainWindow"
x:DataType="vm:RootViewModel"
Icon="/Assets/avalonia-logo.ico"
Width="1200" Height="740" MinWidth="920" MinHeight="600"
Background="{DynamicResource Bg}"
Title="ЭльФиСА">
<ContentControl Content="{Binding Current}"/>
</Window>

Some files were not shown because too many files have changed in this diff Show More