Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98a024ba10 | |||
|
|
f98978a5d2 | ||
|
|
028c588d2a | ||
|
|
3fc825e54b | ||
|
|
0536d480cb | ||
|
|
cd2fb57bca | ||
|
|
2420a71ace | ||
|
|
c677345ba6 | ||
|
|
43468ac693 | ||
|
|
b13a867f8d | ||
|
|
77dfb08ef4 | ||
|
|
132ce1531f | ||
|
|
f6a1012e9b | ||
|
|
a4880d7c50 | ||
|
|
4b43907353 | ||
|
|
4d398aca84 | ||
|
|
f7fb7ed1b0 | ||
|
|
557f8667ad | ||
|
|
b10eba0984 | ||
|
|
63abfd4e31 | ||
|
|
d2a1a86794 | ||
| 2659de14b9 | |||
|
|
166a431bda | ||
|
|
cdb2d353a5 | ||
|
|
0253bdf022 | ||
|
|
621f6a6cf3 | ||
|
|
03c71b8e78 | ||
|
|
2230d4c73f | ||
|
|
28f0886b32 | ||
|
|
d80953baad | ||
|
|
3039c324be | ||
|
|
7497ee2d4f | ||
|
|
b69e1b4498 | ||
|
|
09535e9b02 | ||
|
|
9dfdb913cb | ||
|
|
d68155b8ef | ||
|
|
3d253a2f54 | ||
|
|
fd5bd18c66 | ||
|
|
458b70e036 | ||
|
|
babc067863 | ||
|
|
e35e67849f | ||
|
|
4b3b2c8ca8 | ||
|
|
7f3bc2eb35 | ||
|
|
5c439fe780 | ||
|
|
999cfe4121 | ||
|
|
1f6f442b21 | ||
|
|
9ee5561f3d | ||
|
|
5e24cc95b0 | ||
|
|
bcd57db153 | ||
|
|
cdfd8353ec | ||
|
|
a6601567f2 | ||
|
|
74390c4918 | ||
|
|
c447048b66 |
10
.gitignore
vendored
@ -20,3 +20,13 @@ Desktop.ini
|
|||||||
efClient.db
|
efClient.db
|
||||||
*.db-journal
|
*.db-journal
|
||||||
**/user.config
|
**/user.config
|
||||||
|
|
||||||
|
packages/
|
||||||
|
installer/output/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
|
||||||
|
# Кросс-платформенные публикации и артефакты сборки
|
||||||
|
publish/
|
||||||
|
build/
|
||||||
|
installer/output/
|
||||||
|
|||||||
6
Elfisa.Cross.slnx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<Solution>
|
||||||
|
<Folder Name="/src/">
|
||||||
|
<Project Path="src/Elfisa.Avalonia/Elfisa.Avalonia.csproj" />
|
||||||
|
<Project Path="src/Elfisa.Core/Elfisa.Core.csproj" />
|
||||||
|
</Folder>
|
||||||
|
</Solution>
|
||||||
@ -29,6 +29,9 @@
|
|||||||
<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>
|
||||||
@ -38,6 +41,21 @@
|
|||||||
<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>
|
||||||
|
|||||||
@ -103,6 +103,7 @@ GET /api/supplier-prices/summary?supplier_id={uuid}
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"markup_percent": 15.5,
|
||||||
"summary": [
|
"summary": [
|
||||||
{
|
{
|
||||||
"supplier_price_id": "uuid",
|
"supplier_price_id": "uuid",
|
||||||
@ -114,6 +115,7 @@ GET /api/supplier-prices/summary?supplier_id={uuid}
|
|||||||
"cure_form": "таб.",
|
"cure_form": "таб.",
|
||||||
"barcode": "...",
|
"barcode": "...",
|
||||||
"price": 123.45,
|
"price": 123.45,
|
||||||
|
"markup_percent": 15.5,
|
||||||
"quantity": 10,
|
"quantity": 10,
|
||||||
"region_id": "...",
|
"region_id": "...",
|
||||||
"region_name": "...",
|
"region_name": "...",
|
||||||
|
|||||||
24
docs/gitea-updates.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
Старый клиент с меньшей версией увидит модалку «Доступно новое обновление» и не пустит в работу.
|
||||||
108
installer/ElfisaPharmacy.iss
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
; ЭльФиСА — установщик «Электронная Фармация»
|
||||||
|
; Кодировка: 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"
|
||||||
24
installer/README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Установщик ЭльФиСА
|
||||||
|
|
||||||
|
## Публичная ссылка (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).
|
||||||
BIN
installer/assets/elfisa-1024.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
installer/assets/elfisa-256.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
installer/assets/elfisa-512.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
installer/assets/elfisa-logo.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
installer/assets/elfisa.ico
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
installer/assets/wizard-side.bmp
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
installer/assets/wizard-small.bmp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
18
installer/license.txt
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ
|
||||||
|
Электронная Фармация (ЭльФиСА)
|
||||||
|
|
||||||
|
1. Программа предоставляется для использования в рамках договора
|
||||||
|
с поставщиком услуг PharmData / 24pharmdata.
|
||||||
|
|
||||||
|
2. Запрещается передавать третьим лицам учётные данные доступа,
|
||||||
|
а также модифицировать клиент способом, нарушающим безопасность
|
||||||
|
обмена данными.
|
||||||
|
|
||||||
|
3. Пользователь несёт ответственность за корректность оформляемых
|
||||||
|
заказов и за сохранность локальных данных на рабочем месте.
|
||||||
|
|
||||||
|
4. Устанавливая программу, вы подтверждаете согласие с условиями
|
||||||
|
использования сервиса и политикой обработки данных, действующими
|
||||||
|
у оператора сервиса.
|
||||||
|
|
||||||
|
При возникновении вопросов обратитесь в службу поддержки.
|
||||||
20
installer/welcome.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
ЭльФиСА — Электронная Фармация
|
||||||
|
================================
|
||||||
|
|
||||||
|
Добро пожаловать в установку настольного клиента «Электронная Фармация».
|
||||||
|
|
||||||
|
Программа предназначена для работы аптек с прайс-листами поставщиков,
|
||||||
|
заказами, накладными и обменом данными с сервером PharmData.
|
||||||
|
|
||||||
|
Скачать установщик всегда можно по адресу:
|
||||||
|
https://cdn.24pharmdata.ru
|
||||||
|
|
||||||
|
Установщик:
|
||||||
|
• скопирует программу в выбранную папку;
|
||||||
|
• предложит ярлык в меню «Пуск»;
|
||||||
|
• по желанию создаст ярлык на рабочем столе.
|
||||||
|
|
||||||
|
Рекомендуется закрыть запущенную «Электронную Фармацию» перед установкой.
|
||||||
|
После установки при первом запуске выполните вход в систему.
|
||||||
|
|
||||||
|
© PharmData / ЭльФиСА
|
||||||
59
scripts/build-installer.ps1
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#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"
|
||||||
@ -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 -->
|
||||||
@ -27,6 +27,9 @@
|
|||||||
<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>
|
||||||
@ -36,6 +39,21 @@
|
|||||||
<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>
|
||||||
</configuration>
|
</configuration>
|
||||||
@ -9,8 +9,91 @@ namespace Электронная_Фармация.Classes
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AppConfig
|
public static class AppConfig
|
||||||
{
|
{
|
||||||
public const string DefaultApiBaseUrl = "http://195.34.241.84:9988";
|
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 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>
|
||||||
|
/// Replaces known dead API URLs in saved user settings.
|
||||||
|
/// </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()
|
||||||
|
{
|
||||||
|
var current = Settings.Default.ApiBaseUrl;
|
||||||
|
if (string.IsNullOrWhiteSpace(current))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = NormalizeApiBaseUrl(current);
|
||||||
|
if (string.Equals(normalized, LegacyApiBaseUrl, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(normalized, "http://195.34.241.84:9988", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Settings.Default.ApiBaseUrl = DefaultApiBaseUrl;
|
||||||
|
Settings.Default.stringToken = string.Empty;
|
||||||
|
Settings.Default.Save();
|
||||||
|
AppDebugLog.Info("AppConfig", $"ApiBaseUrl мигрирован: {LegacyApiBaseUrl} -> {DefaultApiBaseUrl}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string ApiBaseUrl
|
public static string ApiBaseUrl
|
||||||
{
|
{
|
||||||
@ -19,48 +102,262 @@ namespace Электронная_Фармация.Classes
|
|||||||
var fromSettings = Settings.Default.ApiBaseUrl;
|
var fromSettings = Settings.Default.ApiBaseUrl;
|
||||||
if (!string.IsNullOrWhiteSpace(fromSettings))
|
if (!string.IsNullOrWhiteSpace(fromSettings))
|
||||||
{
|
{
|
||||||
return fromSettings.Trim().TrimEnd('/');
|
return NormalizeApiBaseUrl(fromSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fromEnv = Environment.GetEnvironmentVariable(ApiBaseUrlEnvVar);
|
var fromEnv = Environment.GetEnvironmentVariable(ApiBaseUrlEnvVar);
|
||||||
if (!string.IsNullOrWhiteSpace(fromEnv))
|
if (!string.IsNullOrWhiteSpace(fromEnv))
|
||||||
{
|
{
|
||||||
return fromEnv.Trim().TrimEnd('/');
|
return NormalizeApiBaseUrl(fromEnv);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DefaultApiBaseUrl;
|
return DefaultApiBaseUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
return DefaultApiBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
url = url.Trim().TrimEnd('/');
|
||||||
|
if (!url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
url = "http://" + 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 root = Path.Combine(baseDir, "efClient.db");
|
var candidates = new[]
|
||||||
var data = Path.Combine(baseDir, "Data", "efClient.db");
|
|
||||||
if (File.Exists(root))
|
|
||||||
{
|
{
|
||||||
return root;
|
Path.Combine(baseDir, "efClient.db"),
|
||||||
}
|
Path.Combine(baseDir, "Data", "efClient.db")
|
||||||
|
};
|
||||||
|
|
||||||
if (File.Exists(data))
|
foreach (var source in candidates)
|
||||||
{
|
{
|
||||||
return data;
|
if (!File.Exists(source))
|
||||||
}
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return root;
|
File.Copy(source, target, overwrite: false);
|
||||||
|
AppDebugLog.Info("AppConfig", $"SQLite DB migrated: {source} -> {target}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppDebugLog.Info("AppConfig", $"SQLite DB migrate skipped: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string SqliteConnectionString
|
public static string SqliteConnectionString
|
||||||
{
|
{
|
||||||
get { return $"Data Source={SqliteDbPath};Version=3;New=False;"; }
|
get
|
||||||
|
{
|
||||||
|
EnsureSQLiteInteropInBaseDir();
|
||||||
|
return $"Data Source={SqliteDbPath};Version=3;New=False;";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string LocationId
|
public static string LocationId
|
||||||
{
|
{
|
||||||
get { return Settings.Default.LocationId ?? string.Empty; }
|
get { return Settings.Default.LocationId ?? string.Empty; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SetLocationId(string locationId)
|
||||||
|
{
|
||||||
|
Settings.Default.LocationId = (locationId ?? string.Empty).Trim();
|
||||||
|
Settings.Default.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string InvoiceExportPath
|
||||||
|
{
|
||||||
|
get { return Settings.Default.InvoiceExportPath ?? string.Empty; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetInvoiceExportPath(string path)
|
||||||
|
{
|
||||||
|
Settings.Default.InvoiceExportPath = (path ?? string.Empty).Trim();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
234
src/ElectronicPharmacy/Classes/AppDebugLog.cs
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Электронная_Фармация.Classes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// File logger for API/auth/network debugging.
|
||||||
|
/// Logs are written to %LocalAppData%\ЭльФиСА\...\Logs\api-debug-YYYY-MM-DD.log.
|
||||||
|
/// </summary>
|
||||||
|
public static class AppDebugLog
|
||||||
|
{
|
||||||
|
private static readonly object Sync = new object();
|
||||||
|
private const int MaxBodyLength = 8000;
|
||||||
|
private const string DisableEnvVar = "ELF_API_DEBUG";
|
||||||
|
private const string DisableMarkerFile = "api-debug.off";
|
||||||
|
|
||||||
|
public static bool IsEnabled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var env = Environment.GetEnvironmentVariable(DisableEnvVar);
|
||||||
|
if (string.Equals(env, "0", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(env, "false", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var markerPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DisableMarkerFile);
|
||||||
|
return !File.Exists(markerPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string LogDirectory => AppConfig.LogsDirectory;
|
||||||
|
|
||||||
|
public static string CurrentLogFilePath =>
|
||||||
|
Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log");
|
||||||
|
|
||||||
|
public static void Info(string category, string message)
|
||||||
|
{
|
||||||
|
Write("INFO", category, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Warning(string category, string message)
|
||||||
|
{
|
||||||
|
Write("WARN", category, message, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Error(string category, string message, Exception exception = null)
|
||||||
|
{
|
||||||
|
Write("ERROR", category, message, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApiRequest(string method, string url, string requestBody = null, string extra = null)
|
||||||
|
{
|
||||||
|
var message = new StringBuilder();
|
||||||
|
message.Append(method).Append(" ").Append(url);
|
||||||
|
if (!string.IsNullOrWhiteSpace(extra))
|
||||||
|
{
|
||||||
|
message.Append(" | ").Append(extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(requestBody))
|
||||||
|
{
|
||||||
|
message.Append(" | body=").Append(Truncate(SanitizeJson(requestBody)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Write("REQ", "Api", message.ToString(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApiResponse(string method, string url, int statusCode, long elapsedMs, string responseBody = null)
|
||||||
|
{
|
||||||
|
var message = new StringBuilder();
|
||||||
|
message.Append(method).Append(" ").Append(url);
|
||||||
|
message.Append(" -> HTTP ").Append(statusCode);
|
||||||
|
message.Append(" (").Append(elapsedMs).Append(" ms)");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(responseBody))
|
||||||
|
{
|
||||||
|
message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Write(statusCode >= 400 ? "RESP-ERR" : "RESP", "Api", message.ToString(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApiFailure(string method, string url, long elapsedMs, Exception exception)
|
||||||
|
{
|
||||||
|
var message = $"{method} {url} failed after {elapsedMs} ms: {exception?.Message}";
|
||||||
|
Write("FAIL", "Api", message, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Логирует ошибку HTTP-ответа API (4xx/5xx) с телом ответа.
|
||||||
|
/// </summary>
|
||||||
|
public static void ApiHttpError(
|
||||||
|
string method,
|
||||||
|
string url,
|
||||||
|
int statusCode,
|
||||||
|
string responseBody,
|
||||||
|
string context = null)
|
||||||
|
{
|
||||||
|
var message = new StringBuilder();
|
||||||
|
message.Append(method).Append(" ").Append(url);
|
||||||
|
message.Append(" -> HTTP ").Append(statusCode);
|
||||||
|
if (!string.IsNullOrWhiteSpace(context))
|
||||||
|
{
|
||||||
|
message.Append(" | ").Append(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(responseBody))
|
||||||
|
{
|
||||||
|
message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Write("HTTP-ERR", "Api", message.ToString(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string MaskToken(string token)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
|
return "(empty)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.Length <= 8)
|
||||||
|
{
|
||||||
|
return "***";
|
||||||
|
}
|
||||||
|
|
||||||
|
return token.Substring(0, 4) + "..." + token.Substring(token.Length - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SanitizeJson(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sanitized = Regex.Replace(
|
||||||
|
json,
|
||||||
|
@"""password""\s*:\s*""[^""]*""",
|
||||||
|
@"""password"":""***""",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
sanitized = Regex.Replace(
|
||||||
|
sanitized,
|
||||||
|
@"""token""\s*:\s*""([^""]*)""",
|
||||||
|
m => @"""token"":""" + MaskToken(m.Groups[1].Value) + @"""",
|
||||||
|
RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatException(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var current = exception;
|
||||||
|
var depth = 0;
|
||||||
|
while (current != null && depth < 8)
|
||||||
|
{
|
||||||
|
if (depth > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(" -> ");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(current.GetType().Name).Append(": ").Append(current.Message);
|
||||||
|
if (!string.IsNullOrWhiteSpace(current.StackTrace) && depth == 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.Append(current.StackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.InnerException;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Write(string level, string category, string message, Exception exception)
|
||||||
|
{
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var line = new StringBuilder();
|
||||||
|
line.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||||
|
line.Append(" [").Append(level).Append("] ");
|
||||||
|
line.Append("[").Append(category).Append("] ");
|
||||||
|
line.Append(message);
|
||||||
|
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
line.AppendLine();
|
||||||
|
line.Append(FormatException(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(LogDirectory);
|
||||||
|
File.AppendAllText(CurrentLogFilePath, line + Environment.NewLine, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine(line.ToString());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Logging must never break the app.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Truncate(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text) || text.Length <= MaxBodyLength)
|
||||||
|
{
|
||||||
|
return text ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.Substring(0, MaxBodyLength) + "...[truncated]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
287
src/ElectronicPharmacy/Classes/AutoUpdater.cs
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/ElectronicPharmacy/Classes/BuyerDiscountDtos.cs
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
506
src/ElectronicPharmacy/Classes/ConsigneeHelper.cs
Normal file
@ -0,0 +1,506 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,6 +10,8 @@ using System.Text;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Электронная_Фармация.HelpForms;
|
||||||
using Электронная_Фармация.Properties;
|
using Электронная_Фармация.Properties;
|
||||||
|
|
||||||
namespace Электронная_Фармация.Classes
|
namespace Электронная_Фармация.Classes
|
||||||
@ -28,26 +30,33 @@ namespace Электронная_Фармация.Classes
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_token))
|
if (string.IsNullOrWhiteSpace(_token))
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("DataSender", "Отправка заказов: токен отсутствует");
|
||||||
ToastNotification.ShowError("Не найден токен авторизации.");
|
ToastNotification.ShowError("Не найден токен авторизации.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
|
List<BuyerOrderRequest> orders;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
ToastNotification.ShowCustom(
|
orders = FetchDataFromSql(null);
|
||||||
"Не указан location_id. Заполните его в окне авторизации/настроек.",
|
}
|
||||||
Color.DarkOrange,
|
catch (Exception ex)
|
||||||
Color.White);
|
{
|
||||||
|
AppDebugLog.Error("DataSender", "Ошибка подготовки заказов к отправке", ex);
|
||||||
|
ToastNotification.ShowError($"Ошибка подготовки заказов: {ex.Message}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var orders = FetchDataFromSql(null);
|
|
||||||
if (orders.Count == 0)
|
if (orders.Count == 0)
|
||||||
{
|
{
|
||||||
ToastNotification.ShowCustom("Нет новых заказов для отправки.", Color.DarkOrange, Color.White);
|
ToastNotification.ShowCustom("Нет новых заказов для отправки.", Color.DarkOrange, Color.White);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!EnsureOrdersHaveLocationId(orders))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
int failCount = 0;
|
int failCount = 0;
|
||||||
|
|
||||||
@ -87,26 +96,33 @@ namespace Электронная_Фармация.Classes
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_token))
|
if (string.IsNullOrWhiteSpace(_token))
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("DataSender", $"Отправка заказа {localOrderId}: токен отсутствует");
|
||||||
ToastNotification.ShowError("Не найден токен авторизации.");
|
ToastNotification.ShowError("Не найден токен авторизации.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(AppConfig.LocationId))
|
List<BuyerOrderRequest> orders;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
ToastNotification.ShowCustom(
|
orders = FetchDataFromSql(localOrderId);
|
||||||
"Не указан location_id. Заполните его в окне авторизации/настроек.",
|
}
|
||||||
Color.DarkOrange,
|
catch (Exception ex)
|
||||||
Color.White);
|
{
|
||||||
|
AppDebugLog.Error("DataSender", $"Ошибка подготовки заказа {localOrderId}", ex);
|
||||||
|
ToastNotification.ShowError($"Ошибка подготовки заказа: {ex.Message}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var orders = FetchDataFromSql(localOrderId);
|
|
||||||
if (orders.Count == 0)
|
if (orders.Count == 0)
|
||||||
{
|
{
|
||||||
ToastNotification.ShowCustom("Заказ не найден или уже отправлен.", Color.DarkOrange, Color.White);
|
ToastNotification.ShowCustom("Заказ не найден или уже отправлен.", Color.DarkOrange, Color.White);
|
||||||
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)
|
||||||
{
|
{
|
||||||
@ -119,7 +135,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>();
|
||||||
string locationId = AppConfig.LocationId;
|
var locationCache = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||||
{
|
{
|
||||||
@ -127,7 +143,11 @@ 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.es_code,
|
||||||
OrderItems.Zakaz,
|
OrderItems.Zakaz,
|
||||||
Orders.Comment
|
Orders.Comment
|
||||||
FROM Orders
|
FROM Orders
|
||||||
@ -153,7 +173,15 @@ 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 esCode = reader["es_code"] == DBNull.Value ? null : reader["es_code"].ToString().Trim();
|
||||||
decimal qty = ReadDecimal(reader["Zakaz"]);
|
decimal qty = ReadDecimal(reader["Zakaz"]);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(supplierPriceId))
|
if (string.IsNullOrWhiteSpace(supplierPriceId))
|
||||||
@ -168,9 +196,13 @@ 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()
|
||||||
};
|
};
|
||||||
@ -180,7 +212,9 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
order.Items.Add(new BuyerOrderItem
|
order.Items.Add(new BuyerOrderItem
|
||||||
{
|
{
|
||||||
SupplierPriceId = supplierPriceId,
|
SupplierPriceId = supplierPriceId,
|
||||||
Qty = qty
|
Qty = qty,
|
||||||
|
ItemName = string.IsNullOrWhiteSpace(drugName) ? null : drugName,
|
||||||
|
ItemCode = string.IsNullOrWhiteSpace(esCode) ? null : esCode
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,6 +224,65 @@ 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
|
||||||
@ -197,6 +290,10 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
};
|
};
|
||||||
string json = JsonSerializer.Serialize(order, options);
|
string json = JsonSerializer.Serialize(order, options);
|
||||||
|
var fullUrl = new Uri(new Uri(_baseUrl.TrimEnd('/') + "/"), BuyerOrdersEndpoint.TrimStart('/')).ToString();
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
|
AppDebugLog.ApiRequest("POST", fullUrl, json, $"order={order.LocalOrderId}");
|
||||||
|
|
||||||
using (var client = new HttpClient())
|
using (var client = new HttpClient())
|
||||||
{
|
{
|
||||||
@ -204,22 +301,126 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token);
|
||||||
|
|
||||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
HttpResponseMessage response = await client.PostAsync(BuyerOrdersEndpoint, content);
|
try
|
||||||
string responseString = await response.Content.ReadAsStringAsync();
|
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
ToastNotification.ShowError(
|
HttpResponseMessage response = await client.PostAsync(BuyerOrdersEndpoint, content);
|
||||||
$"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}");
|
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,
|
||||||
|
$"order={order.LocalOrderId}");
|
||||||
|
AppDebugLog.Error(
|
||||||
|
"DataSender",
|
||||||
|
$"Ошибка отправки заказа {order.LocalOrderId}: HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||||
|
ToastNotification.ShowError(
|
||||||
|
$"Ошибка отправки заказа {order.LocalOrderId}: {(int)response.StatusCode} {response.ReasonPhrase}\n{responseString}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var createdOrder = JsonSerializer.Deserialize<BuyerOrderCreateResult>(responseString, options);
|
||||||
|
if (createdOrder == null || string.IsNullOrWhiteSpace(createdOrder.OrderID))
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppDebugLog.Error("DataSender", $"Ошибка отправки заказа {order.LocalOrderId}", ex);
|
||||||
|
ToastNotification.ShowError(ex.Message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
AppDebugLog.ApiFailure("POST", fullUrl, stopwatch.ElapsedMilliseconds, ex);
|
||||||
|
ToastNotification.ShowError($"Ошибка сети при отправке заказа {order.LocalOrderId}: {ex.Message}");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
MarkOrderSent(order.LocalOrderId);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MarkOrderSent(string localOrderId)
|
private async Task<string> PlaceOrderAsync(HttpClient client, string remoteOrderId)
|
||||||
|
{
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
@ -230,9 +431,20 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
{
|
{
|
||||||
con.Open();
|
con.Open();
|
||||||
using (var cmd = new SQLiteCommand(
|
using (var cmd = new SQLiteCommand(
|
||||||
"UPDATE [Orders] SET [OrderState] = 'ОТПРАВЛЕН' WHERE [Id_Order] = @id AND [OrderState] = 'НОВЫЙ'",
|
@"UPDATE [Orders]
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
@ -269,6 +481,9 @@ 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; }
|
||||||
|
|
||||||
@ -283,5 +498,23 @@ WHERE Orders.OrderState = 'НОВЫЙ'
|
|||||||
|
|
||||||
[JsonPropertyName("qty")]
|
[JsonPropertyName("qty")]
|
||||||
public decimal Qty { get; set; }
|
public decimal Qty { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("item_name")]
|
||||||
|
public string ItemName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("item_code")]
|
||||||
|
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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
321
src/ElectronicPharmacy/Classes/DiscountResolver.cs
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
194
src/ElectronicPharmacy/Classes/InvoiceExportService.cs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using System.Data.SQLite;
|
||||||
|
|
||||||
|
namespace Электронная_Фармация.Classes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exports a local invoice (header + items) to a CSV file in the configured folder.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class InvoiceExportService
|
||||||
|
{
|
||||||
|
public string ExportInvoice(string invoiceId, IWin32Window owner = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(invoiceId))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Не указана накладная для экспорта.", nameof(invoiceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
var folder = ResolveExportFolder(owner);
|
||||||
|
if (string.IsNullOrWhiteSpace(folder))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Не указана папка для экспорта накладных.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
InvoiceHeader header;
|
||||||
|
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||||
|
{
|
||||||
|
con.Open();
|
||||||
|
header = LoadHeader(con, invoiceId);
|
||||||
|
var itemsSql = @"
|
||||||
|
select
|
||||||
|
ifnull([GoodCode], '') as [GoodCode],
|
||||||
|
ifnull([Good], '') as [Good],
|
||||||
|
ifnull([ProducerName], '') as [ProducerName],
|
||||||
|
ifnull([GoodsGount], 0) as [Qty],
|
||||||
|
ifnull([PriceSupplierWithNDS], ifnull([PriceSupplierWithoutNDS], 0)) as [Price],
|
||||||
|
ifnull([SumWithNDS], ifnull([SumWithoutNDS], 0)) as [Sum],
|
||||||
|
ifnull([BestBefore], '') as [BestBefore],
|
||||||
|
ifnull([Serial], '') as [Serial]
|
||||||
|
from [InvoiceItem]
|
||||||
|
where [idInvoice] = @invoiceId
|
||||||
|
order by [Good];";
|
||||||
|
|
||||||
|
var fileName = BuildFileName(header);
|
||||||
|
var filePath = Path.Combine(folder, fileName);
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("Номер накладной;Дата;Поставщик;Грузополучатель;Номер заказа;Сумма");
|
||||||
|
sb.AppendLine(string.Join(";",
|
||||||
|
Csv(header.Number),
|
||||||
|
Csv(header.Date),
|
||||||
|
Csv(header.Supplier),
|
||||||
|
Csv(header.Consignee),
|
||||||
|
Csv(header.OrderNumber),
|
||||||
|
Csv(header.Sum)));
|
||||||
|
sb.AppendLine();
|
||||||
|
sb.AppendLine("Код товара;Товар;Производитель;Количество;Цена;Сумма;Срок годности;Серия");
|
||||||
|
|
||||||
|
using (var cmd = new SQLiteCommand(itemsSql, con))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
sb.AppendLine(string.Join(";",
|
||||||
|
Csv(reader["GoodCode"]),
|
||||||
|
Csv(reader["Good"]),
|
||||||
|
Csv(reader["ProducerName"]),
|
||||||
|
Csv(reader["Qty"]),
|
||||||
|
Csv(reader["Price"]),
|
||||||
|
Csv(reader["Sum"]),
|
||||||
|
Csv(reader["BestBefore"]),
|
||||||
|
Csv(reader["Serial"])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UTF-8 BOM — Excel корректно открывает кириллицу.
|
||||||
|
File.WriteAllText(filePath, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
|
||||||
|
AppDebugLog.Info("InvoiceExport", $"Накладная {header.Number} экспортирована: {filePath}");
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveExportFolder(IWin32Window owner = null)
|
||||||
|
{
|
||||||
|
var folder = AppConfig.InvoiceExportPath;
|
||||||
|
if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var dialog = new FolderBrowserDialog())
|
||||||
|
{
|
||||||
|
dialog.Description = "Выберите папку для экспорта накладных";
|
||||||
|
dialog.ShowNewFolderButton = true;
|
||||||
|
if (!string.IsNullOrWhiteSpace(folder) && Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
dialog.SelectedPath = folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.ShowDialog(owner) != DialogResult.OK || string.IsNullOrWhiteSpace(dialog.SelectedPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig.SetInvoiceExportPath(dialog.SelectedPath);
|
||||||
|
return dialog.SelectedPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InvoiceHeader LoadHeader(SQLiteConnection con, string invoiceId)
|
||||||
|
{
|
||||||
|
using (var cmd = new SQLiteCommand(@"
|
||||||
|
select
|
||||||
|
ifnull([InvoiceNumber], '') as [Number],
|
||||||
|
ifnull([InvoiceDate], '') as [Date],
|
||||||
|
ifnull([SupplierName], '') as [Supplier],
|
||||||
|
ifnull([ConsigneesName], '') as [Consignee],
|
||||||
|
ifnull([SourceOrderNumber], '') as [OrderNumber],
|
||||||
|
ifnull([InvoiceSum], ifnull([SumWithNDS], 0)) as [Sum]
|
||||||
|
from [Invoice]
|
||||||
|
where [idInvoice] = @invoiceId
|
||||||
|
limit 1;", con))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
if (!reader.Read())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Накладная не найдена.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new InvoiceHeader
|
||||||
|
{
|
||||||
|
Number = reader["Number"]?.ToString() ?? string.Empty,
|
||||||
|
Date = reader["Date"]?.ToString() ?? string.Empty,
|
||||||
|
Supplier = reader["Supplier"]?.ToString() ?? string.Empty,
|
||||||
|
Consignee = reader["Consignee"]?.ToString() ?? string.Empty,
|
||||||
|
OrderNumber = reader["OrderNumber"]?.ToString() ?? string.Empty,
|
||||||
|
Sum = reader["Sum"]?.ToString() ?? "0"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildFileName(InvoiceHeader header)
|
||||||
|
{
|
||||||
|
var number = SanitizeFileName(string.IsNullOrWhiteSpace(header.Number) ? "invoice" : header.Number);
|
||||||
|
var stamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
|
||||||
|
return $"Накладная_{number}_{stamp}.csv";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileName(string value)
|
||||||
|
{
|
||||||
|
foreach (var c in Path.GetInvalidFileNameChars())
|
||||||
|
{
|
||||||
|
value = value.Replace(c, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Csv(object value)
|
||||||
|
{
|
||||||
|
var text = value?.ToString() ?? string.Empty;
|
||||||
|
if (text.IndexOfAny(new[] { ';', '"', '\r', '\n' }) >= 0)
|
||||||
|
{
|
||||||
|
return "\"" + text.Replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class InvoiceHeader
|
||||||
|
{
|
||||||
|
public string Number { get; set; }
|
||||||
|
public string Date { get; set; }
|
||||||
|
public string Supplier { get; set; }
|
||||||
|
public string Consignee { get; set; }
|
||||||
|
public string OrderNumber { get; set; }
|
||||||
|
public string Sum { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
340
src/ElectronicPharmacy/Classes/InvoiceRequestService.cs
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.SQLite;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Электронная_Фармация.Classes
|
||||||
|
{
|
||||||
|
public sealed class InvoiceRequestService
|
||||||
|
{
|
||||||
|
public string CreateOrUpdateFromOrder(string orderId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(orderId))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("Не указан заказ для формирования накладной.", nameof(orderId));
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||||
|
{
|
||||||
|
con.Open();
|
||||||
|
|
||||||
|
string orderNumber;
|
||||||
|
string supplierName;
|
||||||
|
string consigneeName;
|
||||||
|
string orderState;
|
||||||
|
|
||||||
|
using (var cmd = new SQLiteCommand(@"
|
||||||
|
select
|
||||||
|
o.[OrderNumber],
|
||||||
|
o.[SupplierName],
|
||||||
|
ifnull(o.[ConsigneeName], '') as [ConsigneeName],
|
||||||
|
ifnull(o.[OrderState], '') as [OrderState]
|
||||||
|
from [Orders] o
|
||||||
|
where o.[Id_Order] = @orderId;", con))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@orderId", orderId);
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
if (!reader.Read())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Выбранный заказ не найден.");
|
||||||
|
}
|
||||||
|
|
||||||
|
orderNumber = reader["OrderNumber"]?.ToString() ?? string.Empty;
|
||||||
|
supplierName = reader["SupplierName"]?.ToString() ?? string.Empty;
|
||||||
|
consigneeName = reader["ConsigneeName"]?.ToString() ?? string.Empty;
|
||||||
|
orderState = reader["OrderState"]?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(orderState, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Получить накладную можно только для заказа со статусом ОТПРАВЛЕН.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<InvoiceOrderItem>();
|
||||||
|
using (var cmd = new SQLiteCommand(@"
|
||||||
|
select
|
||||||
|
ifnull([es_code], 0) as [GoodCode],
|
||||||
|
[DrugName],
|
||||||
|
[Price],
|
||||||
|
ifnull([Zakaz], 0) as [Zakaz],
|
||||||
|
ifnull([RefuseItemsCount], 0) as [RefuseItemsCount],
|
||||||
|
ifnull([ExpiryPeriod], '') as [ExpiryPeriod]
|
||||||
|
from [OrderItems]
|
||||||
|
where [Id_Order] = @orderId
|
||||||
|
order by [DrugName];", con))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@orderId", orderId);
|
||||||
|
using (var reader = cmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
int orderedQty = SafeInt(reader["Zakaz"]);
|
||||||
|
int refusedQty = SafeInt(reader["RefuseItemsCount"]);
|
||||||
|
int arrivedQty = Math.Max(orderedQty - refusedQty, 0);
|
||||||
|
if (arrivedQty <= 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var price = SafeDecimal(reader["Price"]);
|
||||||
|
items.Add(new InvoiceOrderItem
|
||||||
|
{
|
||||||
|
GoodCode = SafeInt(reader["GoodCode"]),
|
||||||
|
GoodName = reader["DrugName"]?.ToString() ?? string.Empty,
|
||||||
|
Price = price,
|
||||||
|
ArrivedQty = arrivedQty,
|
||||||
|
ExpiryPeriod = reader["ExpiryPeriod"]?.ToString() ?? string.Empty
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Для этого заказа нет фактически пришедших позиций.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int invoiceId;
|
||||||
|
string invoiceNumber;
|
||||||
|
using (var findCmd = new SQLiteCommand(@"
|
||||||
|
select [idInvoice], [InvoiceNumber]
|
||||||
|
from [Invoice]
|
||||||
|
where [SourceOrderId] = @orderId
|
||||||
|
limit 1;", con))
|
||||||
|
{
|
||||||
|
findCmd.Parameters.AddWithValue("@orderId", orderId);
|
||||||
|
using (var reader = findCmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
invoiceId = Convert.ToInt32(reader["idInvoice"]);
|
||||||
|
invoiceNumber = reader["InvoiceNumber"]?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
invoiceId = 0;
|
||||||
|
invoiceNumber = $"НКЛ-{orderNumber}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoiceDate = DateTime.Now.ToString("yyyy-MM-dd");
|
||||||
|
var invoiceSum = 0m;
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
invoiceSum += item.Price * item.ArrivedQty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoiceId == 0)
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
@invoiceSum,
|
||||||
|
0,
|
||||||
|
@sourceOrderId,
|
||||||
|
@sourceOrderNumber,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
select last_insert_rowid();", con))
|
||||||
|
{
|
||||||
|
insertCmd.Parameters.AddWithValue("@number", invoiceNumber);
|
||||||
|
insertCmd.Parameters.AddWithValue("@date", invoiceDate);
|
||||||
|
insertCmd.Parameters.AddWithValue("@sum", invoiceSum);
|
||||||
|
insertCmd.Parameters.AddWithValue("@supplier", supplierName);
|
||||||
|
insertCmd.Parameters.AddWithValue("@consignee", consigneeName);
|
||||||
|
insertCmd.Parameters.AddWithValue("@invoiceSum", invoiceSum);
|
||||||
|
insertCmd.Parameters.AddWithValue("@sourceOrderId", orderId);
|
||||||
|
insertCmd.Parameters.AddWithValue("@sourceOrderNumber", orderNumber);
|
||||||
|
invoiceId = Convert.ToInt32(insertCmd.ExecuteScalar());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using (var updateCmd = new SQLiteCommand(@"
|
||||||
|
update [Invoice]
|
||||||
|
set
|
||||||
|
[InvoiceDate] = @date,
|
||||||
|
[SumWithoutNDS] = @sum,
|
||||||
|
[SumWithNDS] = @sum,
|
||||||
|
[SumNDS] = 0,
|
||||||
|
[SupplierName] = @supplier,
|
||||||
|
[ConsigneesName] = @consignee,
|
||||||
|
[InvoiceSum] = @invoiceSum,
|
||||||
|
[RefuseSum] = 0,
|
||||||
|
[SourceOrderNumber] = @sourceOrderNumber,
|
||||||
|
[IsRequestedInvoice] = 1
|
||||||
|
where [idInvoice] = @invoiceId;", con))
|
||||||
|
{
|
||||||
|
updateCmd.Parameters.AddWithValue("@date", invoiceDate);
|
||||||
|
updateCmd.Parameters.AddWithValue("@sum", invoiceSum);
|
||||||
|
updateCmd.Parameters.AddWithValue("@supplier", supplierName);
|
||||||
|
updateCmd.Parameters.AddWithValue("@consignee", consigneeName);
|
||||||
|
updateCmd.Parameters.AddWithValue("@invoiceSum", invoiceSum);
|
||||||
|
updateCmd.Parameters.AddWithValue("@sourceOrderNumber", orderNumber);
|
||||||
|
updateCmd.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||||
|
updateCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var deleteItemsCmd = new SQLiteCommand("delete from [InvoiceItem] where [idInvoice] = @invoiceId;", con))
|
||||||
|
{
|
||||||
|
deleteItemsCmd.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||||
|
deleteItemsCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
decimal lineSum = item.Price * item.ArrivedQty;
|
||||||
|
using (var itemCmd = 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,
|
||||||
|
'',
|
||||||
|
@bestBefore,
|
||||||
|
'',
|
||||||
|
0,
|
||||||
|
''
|
||||||
|
);", con))
|
||||||
|
{
|
||||||
|
itemCmd.Parameters.AddWithValue("@goodCode", item.GoodCode);
|
||||||
|
itemCmd.Parameters.AddWithValue("@good", item.GoodName);
|
||||||
|
itemCmd.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||||
|
itemCmd.Parameters.AddWithValue("@qty", item.ArrivedQty);
|
||||||
|
itemCmd.Parameters.AddWithValue("@price", item.Price);
|
||||||
|
itemCmd.Parameters.AddWithValue("@sum", lineSum);
|
||||||
|
itemCmd.Parameters.AddWithValue("@bestBefore", item.ExpiryPeriod);
|
||||||
|
itemCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SafeInt(object value)
|
||||||
|
{
|
||||||
|
if (value == null || value == DBNull.Value)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int result;
|
||||||
|
return int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result)
|
||||||
|
|| int.TryParse(value.ToString(), NumberStyles.Integer, CultureInfo.CurrentCulture, out result)
|
||||||
|
? result
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal SafeDecimal(object value)
|
||||||
|
{
|
||||||
|
if (value == null || value == DBNull.Value)
|
||||||
|
{
|
||||||
|
return 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = value.ToString()?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
decimal result;
|
||||||
|
if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.Replace(',', '.');
|
||||||
|
if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decimal.TryParse(text, NumberStyles.Number, CultureInfo.CurrentCulture, out result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class InvoiceOrderItem
|
||||||
|
{
|
||||||
|
public int GoodCode { get; set; }
|
||||||
|
public string GoodName { get; set; }
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
public int ArrivedQty { get; set; }
|
||||||
|
public string ExpiryPeriod { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,62 +1,289 @@
|
|||||||
using System.Collections.Generic;
|
using System;
|
||||||
using System.Data.SQLite;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Data.SQLite;
|
||||||
using Электронная_Фармация.Properties;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
namespace Электронная_Фармация.Classes
|
using System.Threading.Tasks;
|
||||||
{
|
using Электронная_Фармация.Properties;
|
||||||
/// <summary>
|
|
||||||
/// Synchronizes electronic invoices and refusals from API into local SQLite.
|
namespace Электронная_Фармация.Classes
|
||||||
/// </summary>
|
{
|
||||||
public sealed class InvoiceSyncService
|
/// <summary>
|
||||||
{
|
/// Synchronizes electronic invoices and refusals from API into local SQLite.
|
||||||
public async Task<int> SyncFromServerAsync()
|
/// </summary>
|
||||||
{
|
public sealed class InvoiceSyncService
|
||||||
var token = Settings.Default.stringToken;
|
{
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
public async Task<int> SyncFromServerAsync()
|
||||||
{
|
{
|
||||||
throw new System.InvalidOperationException("Необходима авторизация перед загрузкой накладных.");
|
var token = Settings.Default.stringToken;
|
||||||
}
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
{
|
||||||
var client = new ApiClient();
|
var ex = new InvalidOperationException("Необходима авторизация перед загрузкой накладных.");
|
||||||
client.SetToken(token);
|
AppDebugLog.Error("InvoiceSync", "Синхронизация накладных без токена", ex);
|
||||||
var items = await client.GetInvoicesAsync();
|
throw ex;
|
||||||
return SaveInvoices(items);
|
}
|
||||||
}
|
|
||||||
|
try
|
||||||
private static int SaveInvoices(IReadOnlyList<InvoiceApiItem> items)
|
{
|
||||||
{
|
var client = new ApiClient();
|
||||||
if (items.Count == 0)
|
client.SetToken(token);
|
||||||
{
|
AppDebugLog.Info("InvoiceSync", $"Старт синхронизации накладных. Сервер={client.BaseUrl}");
|
||||||
return 0;
|
var orders = await client.GetBuyerOrdersAsync(limit: 200);
|
||||||
}
|
var items = new List<InvoiceApiItem>();
|
||||||
|
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||||
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
{
|
||||||
{
|
con.Open();
|
||||||
con.Open();
|
foreach (var order in orders)
|
||||||
using (var tx = con.BeginTransaction())
|
{
|
||||||
{
|
if (string.Equals(order.Status, "Cancelled", StringComparison.OrdinalIgnoreCase))
|
||||||
foreach (var item in items)
|
{
|
||||||
{
|
continue;
|
||||||
using (var cmd = new SQLiteCommand(@"
|
}
|
||||||
INSERT OR REPLACE INTO Invoice (InvoiceNumber, invoiceDate, SupplierName, ConsigneesName, InvoiceSum, RefuseSum)
|
|
||||||
VALUES (@number, @date, @supplier, @consignee, @sum, @refuseSum)", con, tx))
|
var detail = await client.GetBuyerOrderByIdAsync(order.OrderID);
|
||||||
{
|
if (detail == null)
|
||||||
cmd.Parameters.AddWithValue("@number", item.InvoiceNumber ?? string.Empty);
|
{
|
||||||
cmd.Parameters.AddWithValue("@date", item.InvoiceDate ?? string.Empty);
|
continue;
|
||||||
cmd.Parameters.AddWithValue("@supplier", item.SupplierName ?? string.Empty);
|
}
|
||||||
cmd.Parameters.AddWithValue("@consignee", item.ConsigneeName ?? string.Empty);
|
|
||||||
cmd.Parameters.AddWithValue("@sum", item.InvoiceSum ?? string.Empty);
|
detail.LocalOrderNumber = TryGetLocalOrderNumber(con, detail.OrderID);
|
||||||
cmd.Parameters.AddWithValue("@refuseSum", item.RefuseSum ?? string.Empty);
|
items.Add(InvoiceApiItem.FromBuyerOrder(detail));
|
||||||
cmd.ExecuteNonQuery();
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
var saved = SaveInvoices(items);
|
||||||
tx.Commit();
|
AppDebugLog.Info("InvoiceSync", $"Накладные сохранены локально: {saved} шт.");
|
||||||
}
|
return saved;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
return items.Count;
|
{
|
||||||
}
|
AppDebugLog.Error("InvoiceSync", "Ошибка синхронизации накладных с API", ex);
|
||||||
}
|
throw;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SaveInvoices(IReadOnlyList<InvoiceApiItem> items)
|
||||||
|
{
|
||||||
|
using (var con = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||||
|
{
|
||||||
|
con.Open();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var invoiceId = UpsertInvoice(con, tx, item);
|
||||||
|
SaveInvoiceItems(con, tx, invoiceId, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
98
src/ElectronicPharmacy/Classes/MarkupHelper.cs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Электронная_Фармация.Classes
|
||||||
|
{
|
||||||
|
public static class MarkupHelper
|
||||||
|
{
|
||||||
|
public const decimal FallbackMarkupPercent = 0m;
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
var total = SumDiscountPercents(itemMarkup, clientMarkup);
|
||||||
|
if (total > 0m)
|
||||||
|
{
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackMarkup > 0m ? fallbackMarkup : FallbackMarkupPercent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static decimal ApplyMarkup(decimal basePrice, decimal markupPercent)
|
||||||
|
{
|
||||||
|
return ApplyDiscount(basePrice, markupPercent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
markupPercent = 0m;
|
||||||
|
if (value == null || value == DBNull.Value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is decimal decimalValue)
|
||||||
|
{
|
||||||
|
markupPercent = decimalValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is double doubleValue)
|
||||||
|
{
|
||||||
|
markupPercent = (decimal)doubleValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is float floatValue)
|
||||||
|
{
|
||||||
|
markupPercent = (decimal)floatValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is int intValue)
|
||||||
|
{
|
||||||
|
markupPercent = intValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = value.ToString()?.Trim().Replace(',', '.');
|
||||||
|
return !string.IsNullOrWhiteSpace(text) &&
|
||||||
|
decimal.TryParse(
|
||||||
|
text,
|
||||||
|
System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out markupPercent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
src/ElectronicPharmacy/Classes/OrderNumberHelper.cs
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,9 @@ 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; }
|
||||||
|
|
||||||
@ -56,11 +59,17 @@ namespace Электронная_Фармация.Classes
|
|||||||
public string Barcode { get; set; }
|
public string Barcode { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Финальная цена с учетом наценки (рассчитана на сервере)
|
/// Базовая цена поставщика (без розничной наценки клиента).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonPropertyName("price")]
|
[JsonPropertyName("price")]
|
||||||
public decimal? Price { get; set; }
|
public decimal? Price { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Процент наценки для позиции. Если не задан — используется markup_percent из ответа прайса.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("markup_percent")]
|
||||||
|
public decimal? MarkupPercent { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Количество товара на складе
|
/// Количество товара на складе
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -9,6 +9,12 @@ namespace Электронная_Фармация.Classes
|
|||||||
{
|
{
|
||||||
public class PriceSummaryResponse
|
public class PriceSummaryResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Процент наценки для текущего клиента (покупателя), приходит вместе с прайсом.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("markup_percent")]
|
||||||
|
public decimal? MarkupPercent { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("summary")]
|
[JsonPropertyName("summary")]
|
||||||
public List<PriceSummaryItem> Summary { get; set; }
|
public List<PriceSummaryItem> Summary { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
153
src/ElectronicPharmacy/Classes/SqlSyntaxHighlighter.cs
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -75,6 +75,14 @@ namespace Электронная_Фармация.Classes
|
|||||||
textBox.BorderStyle = BorderStyle.FixedSingle;
|
textBox.BorderStyle = BorderStyle.FixedSingle;
|
||||||
textBox.Font = theme.FontRegular13;
|
textBox.Font = theme.FontRegular13;
|
||||||
}
|
}
|
||||||
|
else if (control is RichTextBox richTextBox)
|
||||||
|
{
|
||||||
|
richTextBox.BackColor = theme.BackgroundPrimary;
|
||||||
|
richTextBox.ForeColor = theme.TextPrimary;
|
||||||
|
richTextBox.BorderStyle = BorderStyle.None;
|
||||||
|
richTextBox.Font = theme.FontRegular13;
|
||||||
|
richTextBox.ReadOnly = true;
|
||||||
|
}
|
||||||
else if (control is NumericUpDown numeric)
|
else if (control is NumericUpDown numeric)
|
||||||
{
|
{
|
||||||
numeric.BackColor = theme.InputBackground;
|
numeric.BackColor = theme.InputBackground;
|
||||||
@ -117,15 +125,36 @@ namespace Электронная_Фармация.Classes
|
|||||||
}
|
}
|
||||||
else if (control is Panel || control is UserControl || control is TableLayoutPanel)
|
else if (control is Panel || control is UserControl || control is TableLayoutPanel)
|
||||||
{
|
{
|
||||||
if (control.Tag as string == "keep-bg")
|
var tag = control.Tag as string;
|
||||||
|
if (tag == "keep-bg")
|
||||||
{
|
{
|
||||||
// preserve custom background
|
// preserve custom background
|
||||||
}
|
}
|
||||||
else if (control.Tag as string == "chrome-child")
|
else if (tag == "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;
|
||||||
|
|||||||
302
src/ElectronicPharmacy/Classes/UpdateChecker.cs
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,9 +17,11 @@
|
|||||||
<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>
|
||||||
@ -30,6 +32,7 @@
|
|||||||
</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>
|
||||||
@ -96,6 +99,8 @@
|
|||||||
<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" />
|
||||||
@ -103,11 +108,21 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<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\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\InvoiceRequestService.cs" />
|
||||||
<Compile Include="Classes\InvoiceSyncService.cs" />
|
<Compile Include="Classes\InvoiceSyncService.cs" />
|
||||||
<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\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" />
|
||||||
@ -119,6 +134,7 @@
|
|||||||
<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>
|
||||||
@ -173,12 +189,30 @@
|
|||||||
<Compile Include="HelpForms\HF_DownloadDataFromServer.Designer.cs">
|
<Compile Include="HelpForms\HF_DownloadDataFromServer.Designer.cs">
|
||||||
<DependentUpon>HF_DownloadDataFromServer.cs</DependentUpon>
|
<DependentUpon>HF_DownloadDataFromServer.cs</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="HelpForms\HF_LocationId.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="HelpForms\HF_LocationId.Designer.cs">
|
||||||
|
<DependentUpon>HF_LocationId.cs</DependentUpon>
|
||||||
|
</Compile>
|
||||||
<Compile Include="HelpForms\HF_Registration.cs">
|
<Compile Include="HelpForms\HF_Registration.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
<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>
|
||||||
@ -214,6 +248,9 @@
|
|||||||
<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>
|
||||||
@ -291,9 +328,6 @@
|
|||||||
</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" />
|
||||||
@ -324,4 +358,7 @@
|
|||||||
<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>
|
||||||
9
src/ElectronicPharmacy/Form1.Designer.cs
generated
@ -1,4 +1,4 @@
|
|||||||
namespace Электронная_Фармация
|
namespace Электронная_Фармация
|
||||||
{
|
{
|
||||||
partial class ElectroPharmacy
|
partial class ElectroPharmacy
|
||||||
{
|
{
|
||||||
@ -230,7 +230,7 @@
|
|||||||
this.tsItemInvoices.Name = "tsItemInvoices";
|
this.tsItemInvoices.Name = "tsItemInvoices";
|
||||||
this.tsItemInvoices.Size = new System.Drawing.Size(224, 32);
|
this.tsItemInvoices.Size = new System.Drawing.Size(224, 32);
|
||||||
this.tsItemInvoices.Text = "Накладные";
|
this.tsItemInvoices.Text = "Накладные";
|
||||||
this.tsItemInvoices.Visible = false;
|
this.tsItemInvoices.Visible = true;
|
||||||
this.tsItemInvoices.Click += new System.EventHandler(this.tsItemInvoices_Click);
|
this.tsItemInvoices.Click += new System.EventHandler(this.tsItemInvoices_Click);
|
||||||
//
|
//
|
||||||
// toolStripSeparator1
|
// toolStripSeparator1
|
||||||
@ -381,8 +381,11 @@
|
|||||||
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.FixedSingle;
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
|
||||||
|
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 = "Электронная Фармация";
|
||||||
|
|||||||
@ -23,10 +23,12 @@ 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;
|
||||||
@ -37,6 +39,9 @@ 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;
|
||||||
@ -46,16 +51,22 @@ 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 = "\uE8FD" },
|
new SidebarItem { Key = "price", Text = "Прайс-лист", IconGlyph = "\uE14C" },
|
||||||
new SidebarItem { Key = "orders", Text = "ЗАКАЗЫ", IconGlyph = "\uE719" },
|
new SidebarItem { Key = "orders", Text = "Заказы", IconGlyph = "\uE7BF" },
|
||||||
new SidebarItem { Key = "upload", Text = "ЗАГРУЗИТЬ", IconGlyph = "\uE896" },
|
new SidebarItem { Key = "invoices", Text = "Накладные", IconGlyph = "\uE8A5" },
|
||||||
new SidebarItem { Key = "send", Text = "ОТПРАВИТЬ", IconGlyph = "\uE725" },
|
new SidebarItem { Key = "reports", Text = "Отчёты", IconGlyph = "" },
|
||||||
new SidebarItem { Key = "refusals", Text = "ОТКАЗЫ", IconGlyph = "\uE711" },
|
new SidebarItem { Key = "upload", Text = "Загрузить", IconGlyph = "\uE896" },
|
||||||
new SidebarItem { Key = "directory", Text = "СПРАВОЧНИК", IconGlyph = "\uE8FD" },
|
new SidebarItem { Key = "send", Text = "Отправить", IconGlyph = "\uE725" },
|
||||||
new SidebarItem { Key = "auth", Text = "АВТОРИЗАЦИЯ", IconGlyph = "\uE192" }
|
new SidebarItem { Key = "refusals", Text = "Отказы", IconGlyph = "\uE7BA" },
|
||||||
|
new SidebarItem { Key = "directory", Text = "Справочник", IconGlyph = "\uE8F1" },
|
||||||
|
new SidebarItem { Key = "auth", Text = "Вход", IconGlyph = "\uE192" }
|
||||||
});
|
});
|
||||||
|
|
||||||
_documentTabStrip = new ModernDocumentTabStrip { Name = "documentTabStrip", Dock = DockStyle.Top };
|
_documentTabStrip = new ModernDocumentTabStrip { Name = "documentTabStrip", Dock = DockStyle.Top };
|
||||||
@ -65,16 +76,22 @@ namespace Электронная_Фармация
|
|||||||
{
|
{
|
||||||
Name = "statusFooter",
|
Name = "statusFooter",
|
||||||
Dock = DockStyle.Bottom,
|
Dock = DockStyle.Bottom,
|
||||||
Height = 30,
|
Height = 32,
|
||||||
Tag = "status-footer",
|
Tag = "status-footer",
|
||||||
Padding = new Padding(12, 0, 16, 0)
|
Padding = new Padding(12, 0, 8, 0)
|
||||||
};
|
};
|
||||||
_statusFooter.Paint += (_, e) =>
|
_statusFooter.Paint += (_, e) =>
|
||||||
{
|
{
|
||||||
var border = ThemeManager.Colors.Border;
|
var theme = ThemeManager.Colors;
|
||||||
using (var pen = new Pen(border))
|
var g = e.Graphics;
|
||||||
|
using (var fill = new SolidBrush(theme.CartHeaderBackground))
|
||||||
{
|
{
|
||||||
e.Graphics.DrawLine(pen, 0, 0, _statusFooter.Width, 0);
|
g.FillRectangle(fill, _statusFooter.ClientRectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var pen = new Pen(theme.AccentPrimaryBorder))
|
||||||
|
{
|
||||||
|
g.DrawLine(pen, 0, 0, _statusFooter.Width, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -82,23 +99,49 @@ namespace Электронная_Фармация
|
|||||||
{
|
{
|
||||||
Name = "lblCurrentUser",
|
Name = "lblCurrentUser",
|
||||||
Dock = DockStyle.Fill,
|
Dock = DockStyle.Fill,
|
||||||
TextAlign = ContentAlignment.MiddleRight,
|
TextAlign = ContentAlignment.MiddleLeft,
|
||||||
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("Отладка");
|
||||||
@ -107,6 +150,7 @@ namespace Электронная_Фармация
|
|||||||
{
|
{
|
||||||
contractorsItem,
|
contractorsItem,
|
||||||
consigneesItem,
|
consigneesItem,
|
||||||
|
switchConsigneeItem,
|
||||||
invoicesItem,
|
invoicesItem,
|
||||||
new ToolStripSeparator(),
|
new ToolStripSeparator(),
|
||||||
debugItem
|
debugItem
|
||||||
@ -139,12 +183,24 @@ 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)
|
||||||
@ -165,14 +221,49 @@ namespace Электронная_Фармация
|
|||||||
{
|
{
|
||||||
ThemeManager.CurrentVariant = Settings.Default.UseDarkTheme
|
ThemeManager.CurrentVariant = Settings.Default.UseDarkTheme
|
||||||
? ThemeVariant.DarkProductivity
|
? ThemeVariant.DarkProductivity
|
||||||
: ThemeVariant.PharmaTrust;
|
: ThemeVariant.ClinicalPro;
|
||||||
_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)
|
||||||
@ -182,18 +273,39 @@ 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)
|
||||||
{
|
{
|
||||||
_lblCurrentUser.Text = $"Пользователь: {login}";
|
userPart = $"Пользователь: {login}";
|
||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(login))
|
else if (!string.IsNullOrWhiteSpace(login))
|
||||||
{
|
{
|
||||||
_lblCurrentUser.Text = $"Пользователь: {login} (не авторизован)";
|
userPart = $"Пользователь: {login} (не авторизован)";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_lblCurrentUser.Text = "Пользователь не авторизован";
|
userPart = "Пользователь не авторизован";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasToken && !string.IsNullOrWhiteSpace(pharmacy) && pharmacy != "Прайс-Лист")
|
||||||
|
{
|
||||||
|
_lblCurrentUser.Text = $"{userPart} | Аптека: {pharmacy}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_lblCurrentUser.Text = $"{userPart} | Аптека: не выбрана";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,6 +319,12 @@ namespace Электронная_Фармация
|
|||||||
case "orders":
|
case "orders":
|
||||||
OpenOrdersTab();
|
OpenOrdersTab();
|
||||||
break;
|
break;
|
||||||
|
case "invoices":
|
||||||
|
OpenInvoicesTab();
|
||||||
|
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");
|
||||||
@ -300,6 +418,11 @@ namespace Электронная_Фармация
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sender is Control clicked && HasDedicatedContextMenu(clicked))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_documentCloseTabMenuItem.Enabled = !string.IsNullOrEmpty(_documentTabStrip.ActiveTabId);
|
_documentCloseTabMenuItem.Enabled = !string.IsNullOrEmpty(_documentTabStrip.ActiveTabId);
|
||||||
_documentCloseAllTabsMenuItem.Enabled = _documentTabStrip.Tabs.Count > 0;
|
_documentCloseAllTabsMenuItem.Enabled = _documentTabStrip.Tabs.Count > 0;
|
||||||
|
|
||||||
@ -307,6 +430,19 @@ namespace Электронная_Фармация
|
|||||||
_documentTabMenu.Show(screenPoint);
|
_documentTabMenu.Show(screenPoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasDedicatedContextMenu(Control control)
|
||||||
|
{
|
||||||
|
for (var current = control; current != null; current = current.Parent)
|
||||||
|
{
|
||||||
|
if (current.ContextMenuStrip != null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void CloseDocumentTab(string tabId)
|
private void CloseDocumentTab(string tabId)
|
||||||
{
|
{
|
||||||
if (_documentContents.TryGetValue(tabId, out var control))
|
if (_documentContents.TryGetValue(tabId, out var control))
|
||||||
@ -343,12 +479,36 @@ namespace Электронная_Фармация
|
|||||||
orders.Show();
|
orders.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpenInvoicesTab(string sourceOrderId = null)
|
||||||
|
{
|
||||||
|
var existing = _documentTabStrip.FindByNavKey("invoices");
|
||||||
|
if (existing != null &&
|
||||||
|
_documentContents.TryGetValue(existing.Id, out var existingControl) &&
|
||||||
|
existingControl is UCInvoices existingInvoices)
|
||||||
|
{
|
||||||
|
existingInvoices.SetSourceOrderFilter(sourceOrderId);
|
||||||
|
_documentTabStrip.SelectTab(existing.Id);
|
||||||
|
ShowDocumentTab(existing.Id);
|
||||||
|
_appHeader.SetActive("invoices");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoices = new UCInvoices(sourceOrderId);
|
||||||
|
OpenDocumentTab("Накладные", "invoices", invoices, allowDuplicate: false);
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenRefusalsTab()
|
private void OpenRefusalsTab()
|
||||||
{
|
{
|
||||||
var refusals = new UCRefusals();
|
var refusals = new UCRefusals();
|
||||||
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);
|
||||||
|
|||||||
@ -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,6 +14,7 @@ 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;
|
||||||
@ -66,25 +67,102 @@ 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 consigneeDialog = new HFConsignees { ParentForm = this };
|
var hasToken = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
|
||||||
UiThemeHelper.ApplyToControlTree(consigneeDialog);
|
|
||||||
consigneeDialog.ShowDialog();
|
// Окно выбора грузополучателя открываем только после авторизации.
|
||||||
|
// Если аптек одна — не открываем окно вообще.
|
||||||
|
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 };
|
||||||
|
UiThemeHelper.ApplyToControlTree(consigneeDialog);
|
||||||
|
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 ucPriceList = new UCPriceList("Прайс-Лист");
|
var consignee = !string.IsNullOrWhiteSpace(ConsigneeName)
|
||||||
OpenDocumentTab("Прайс-лист", "price", ucPriceList, allowDuplicate: true);
|
? ConsigneeName
|
||||||
|
: 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 загрузкаПрайсЛиста_старая версия
|
||||||
@ -139,15 +217,26 @@ 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);
|
||||||
fConsignees.ShowDialog();
|
var dr = fConsignees.ShowDialog(this);
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
var ucInvoices = new UCInvoices();
|
OpenInvoicesTab();
|
||||||
OpenDocumentTab("Накладные", "invoices", ucInvoices, allowDuplicate: true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tsItemPriceList_Click(object sender, EventArgs e)
|
private void tsItemPriceList_Click(object sender, EventArgs e)
|
||||||
@ -165,9 +254,38 @@ 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);
|
||||||
OpenDocumentTab($"Прайс-лист ({consignee})", "price", ucPriceList, allowDuplicate: true);
|
foreach (var tab in _documentTabStrip.Tabs.Where(t => t.NavKey == "price").ToList())
|
||||||
|
{
|
||||||
|
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)
|
||||||
@ -185,7 +303,9 @@ 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;
|
||||||
string qShowMeCoountDataFromTempOrder = "select count(*) from TempOrderItems";
|
var consigneeSafe = (string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName)
|
||||||
|
?.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))
|
||||||
@ -209,6 +329,8 @@ 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)
|
||||||
@ -501,6 +623,7 @@ namespace Электронная_Фармация
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("Form1", "Не удалось отправить данные (меню Обмен)", ex);
|
||||||
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
||||||
//MessageBox.Show(ex.ToString(), "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
//MessageBox.Show(ex.ToString(), "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
//MessageBox.Show($"Не удалось отправить данные на сервер. Текст ошибки:\n{ex.ToString()}", "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
//MessageBox.Show($"Не удалось отправить данные на сервер. Текст ошибки:\n{ex.ToString()}", "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
@ -584,6 +707,85 @@ 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();
|
||||||
}
|
}
|
||||||
@ -591,10 +793,38 @@ 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);
|
||||||
|
|
||||||
@ -605,6 +835,7 @@ namespace Электронная_Фармация
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("Form1", "Не удалось отправить данные (кнопка ОТПРАВИТЬ)", ex);
|
||||||
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -617,7 +848,9 @@ 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;
|
||||||
string qShowMeCoountDataFromTempOrder = "select count(*) from TempOrderItems";
|
var consigneeSafe = (string.IsNullOrWhiteSpace(ConsigneeName) ? ConsigneeHelper.GetDefaultConsigneeName() : ConsigneeName)
|
||||||
|
?.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))
|
||||||
@ -641,6 +874,8 @@ 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)
|
||||||
|
|||||||
@ -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,7 +76,8 @@ 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)
|
||||||
)
|
)
|
||||||
";
|
";
|
||||||
|
|
||||||
@ -250,7 +251,8 @@ 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)
|
||||||
)
|
)
|
||||||
";
|
";
|
||||||
|
|
||||||
@ -307,6 +309,7 @@ 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,
|
||||||
@ -318,8 +321,9 @@ create table if not exists [OrderItems] (
|
|||||||
[Description] text,
|
[Description] text,
|
||||||
[Zakaz] real,
|
[Zakaz] real,
|
||||||
[SummaZakaza] nvarchar(64),
|
[SummaZakaza] nvarchar(64),
|
||||||
[id_PriceList_Item] integer primary key not null,
|
[id_PriceList_Item] integer not null,
|
||||||
[supplier_price_id] nvarchar(36)
|
[supplier_price_id] nvarchar(36),
|
||||||
|
primary key ([ConsigneeName], [id_PriceList_Item])
|
||||||
)
|
)
|
||||||
";
|
";
|
||||||
|
|
||||||
@ -392,10 +396,167 @@ 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", "ConsigneesName", "nvarchar(128)");
|
||||||
|
TryAddColumn(conForCheckTables, "Invoice", "InvoiceSum", "nvarchar(64)");
|
||||||
|
TryAddColumn(conForCheckTables, "Invoice", "RefuseSum", "nvarchar(64)");
|
||||||
|
TryAddColumn(conForCheckTables, "Invoice", "SourceOrderId", "integer");
|
||||||
|
TryAddColumn(conForCheckTables, "Invoice", "SourceOrderNumber", "nvarchar(36)");
|
||||||
|
TryAddColumn(conForCheckTables, "Invoice", "IsRequestedInvoice", "int(1)");
|
||||||
|
TryAddColumn(conForCheckTables, "Orders", "RemoteOrderId", "nvarchar(36)");
|
||||||
|
TryAddColumn(conForCheckTables, "PriceList", "TradeName", "nvarchar(256)");
|
||||||
|
TryAddColumn(conForCheckTables, "PriceList", "Dosage", "nvarchar(128)");
|
||||||
|
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
|
||||||
|
|||||||
99
src/ElectronicPharmacy/Forms/FConsignees.Designer.cs
generated
@ -2,15 +2,8 @@
|
|||||||
{
|
{
|
||||||
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))
|
||||||
@ -22,13 +15,12 @@
|
|||||||
|
|
||||||
#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();
|
||||||
@ -39,31 +31,67 @@
|
|||||||
// 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.Location = new System.Drawing.Point(13, 14);
|
this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
this.panel1.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
|
this.panel1.Location = new System.Drawing.Point(0, 0);
|
||||||
|
this.panel1.Margin = new System.Windows.Forms.Padding(4);
|
||||||
this.panel1.Name = "panel1";
|
this.panel1.Name = "panel1";
|
||||||
this.panel1.Size = new System.Drawing.Size(724, 423);
|
this.panel1.Padding = new System.Windows.Forms.Padding(12);
|
||||||
|
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.Font = new System.Drawing.Font("Segoe UI", 11.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
|
this.txtInteractiveShowMeConsignees.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||||
this.txtInteractiveShowMeConsignees.Location = new System.Drawing.Point(91, 6);
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
this.txtInteractiveShowMeConsignees.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
|
this.txtInteractiveShowMeConsignees.Font = new System.Drawing.Font("Segoe UI", 11.25F);
|
||||||
|
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(623, 32);
|
this.txtInteractiveShowMeConsignees.Size = new System.Drawing.Size(700, 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, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(204)));
|
this.label2.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||||
this.label2.Location = new System.Drawing.Point(5, 6);
|
this.label2.Location = new System.Drawing.Point(16, 18);
|
||||||
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;
|
||||||
@ -73,45 +101,48 @@
|
|||||||
//
|
//
|
||||||
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(11, 44);
|
this.dgvConsignees.Location = new System.Drawing.Point(16, 56);
|
||||||
this.dgvConsignees.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
|
|
||||||
this.dgvConsignees.Name = "dgvConsignees";
|
this.dgvConsignees.Name = "dgvConsignees";
|
||||||
this.dgvConsignees.ReadOnly = true;
|
this.dgvConsignees.ReadOnly = false;
|
||||||
this.dgvConsignees.RowHeadersWidthSizeMode = System.Windows.Forms.DataGridViewRowHeadersWidthSizeMode.AutoSizeToDisplayedHeaders;
|
this.dgvConsignees.RowHeadersWidthSizeMode = System.Windows.Forms.DataGridViewRowHeadersWidthSizeMode.AutoSizeToDisplayedHeaders;
|
||||||
this.dgvConsignees.Size = new System.Drawing.Size(704, 369);
|
this.dgvConsignees.Size = new System.Drawing.Size(775, 360);
|
||||||
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(749, 450);
|
this.ClientSize = new System.Drawing.Size(820, 480);
|
||||||
this.Controls.Add(this.panel1);
|
this.Controls.Add(this.panel1);
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
|
||||||
this.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
this.MaximizeBox = true;
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,41 +1,43 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Drawing;
|
using System.Data.SQLite;
|
||||||
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 System.Data.SQLite;
|
using Электронная_Фармация.Properties;
|
||||||
|
|
||||||
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 void FConsignees_Load(object sender, EventArgs e)
|
private async 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 [codeConsignees] as [Код грузополучателя],
|
select [idConsignees] as [Id],
|
||||||
|
[codeConsignees] as [Код],
|
||||||
[ConsigneesName] as [Наименование],
|
[ConsigneesName] as [Наименование],
|
||||||
[ConsigneesAddress] as [Адрес],
|
[ConsigneesAddress] as [Адрес],
|
||||||
[ConsigneesUseAsDefault] as [По умолчанию?]
|
ifnull([LocationId], '') as [Location ID],
|
||||||
|
[ConsigneesUseAsDefault] as [По умолчанию]
|
||||||
from [Consignees]
|
from [Consignees]
|
||||||
order by [ConsigneesUseAsDefault] desc";
|
order by [ConsigneesUseAsDefault] desc, [ConsigneesName]";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
conConsignees.Open();
|
conConsignees.Open();
|
||||||
@ -44,16 +46,21 @@ order by [ConsigneesUseAsDefault] desc";
|
|||||||
SQLiteDataAdapter daConsigneeList = new SQLiteDataAdapter(cmdShowConsigneesList);
|
SQLiteDataAdapter daConsigneeList = new SQLiteDataAdapter(cmdShowConsigneesList);
|
||||||
daConsigneeList.Fill(tableConsignees);
|
daConsigneeList.Fill(tableConsignees);
|
||||||
dgvConsignees.DataSource = tableConsignees;
|
dgvConsignees.DataSource = tableConsignees;
|
||||||
dgvConsignees.Columns[3].ValueType = typeof(float);
|
if (dgvConsignees.Columns.Contains("Id"))
|
||||||
|
{
|
||||||
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,9 +68,9 @@ order by [ConsigneesUseAsDefault] desc";
|
|||||||
{
|
{
|
||||||
if (e.KeyChar == (char)Keys.Enter)
|
if (e.KeyChar == (char)Keys.Enter)
|
||||||
{
|
{
|
||||||
if (dgvConsignees.Rows.Count > 0)
|
if (dgvConsignees.Rows.Count > 0 && dgvConsignees.DataSource is DataTable table)
|
||||||
{
|
{
|
||||||
(dgvConsignees.DataSource as DataTable).DefaultView.RowFilter = string.Format($"[Наименование] like '%{txtInteractiveShowMeConsignees.Text}%'");
|
table.DefaultView.RowFilter = $"[Наименование] like '%{txtInteractiveShowMeConsignees.Text.Replace("'", "''")}%'";
|
||||||
if (dgvConsignees.Rows.Count == 0)
|
if (dgvConsignees.Rows.Count == 0)
|
||||||
{
|
{
|
||||||
showMeContentFromConsigneesTable();
|
showMeContentFromConsigneesTable();
|
||||||
@ -77,5 +84,132 @@ order by [ConsigneesUseAsDefault] desc";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
295
src/ElectronicPharmacy/Forms/FDebug.Designer.cs
generated
@ -2,15 +2,8 @@
|
|||||||
{
|
{
|
||||||
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))
|
||||||
@ -22,115 +15,267 @@
|
|||||||
|
|
||||||
#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.RTXT_Sql = new System.Windows.Forms.RichTextBox();
|
this.splitMain = new System.Windows.Forms.SplitContainer();
|
||||||
this.label1 = new System.Windows.Forms.Label();
|
this.listTemplates = new System.Windows.Forms.ListBox();
|
||||||
this.btnCleanSqlScript = new Elfisa.UI.Controls.ModernButton();
|
this.lblTemplates = new System.Windows.Forms.Label();
|
||||||
this.btnExecuteSqlScript = new Elfisa.UI.Controls.ModernButton();
|
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.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.lblParams = new System.Windows.Forms.Label();
|
||||||
|
this.panelParams = new System.Windows.Forms.Panel();
|
||||||
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();
|
||||||
//
|
//
|
||||||
// RTXT_Sql
|
// splitMain
|
||||||
//
|
//
|
||||||
this.RTXT_Sql.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
this.splitMain.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
| System.Windows.Forms.AnchorStyles.Right)));
|
this.splitMain.Location = new System.Drawing.Point(0, 0);
|
||||||
this.RTXT_Sql.Location = new System.Drawing.Point(9, 70);
|
this.splitMain.Name = "splitMain";
|
||||||
this.RTXT_Sql.Margin = new System.Windows.Forms.Padding(4);
|
|
||||||
this.RTXT_Sql.Name = "RTXT_Sql";
|
|
||||||
this.RTXT_Sql.Size = new System.Drawing.Size(565, 118);
|
|
||||||
this.RTXT_Sql.TabIndex = 0;
|
|
||||||
this.RTXT_Sql.Text = "";
|
|
||||||
//
|
//
|
||||||
// label1
|
// splitMain.Panel1
|
||||||
//
|
//
|
||||||
this.label1.AutoSize = true;
|
this.splitMain.Panel1.Controls.Add(this.listTemplates);
|
||||||
this.label1.Location = new System.Drawing.Point(6, 50);
|
this.splitMain.Panel1.Controls.Add(this.lblTemplates);
|
||||||
this.label1.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
|
this.splitMain.Panel1MinSize = 180;
|
||||||
this.label1.Name = "label1";
|
|
||||||
this.label1.Size = new System.Drawing.Size(84, 16);
|
|
||||||
this.label1.TabIndex = 1;
|
|
||||||
this.label1.Text = "SQL-запрос";
|
|
||||||
//
|
//
|
||||||
// btnCleanSqlScript
|
// splitMain.Panel2
|
||||||
//
|
//
|
||||||
this.btnCleanSqlScript.Location = new System.Drawing.Point(9, 15);
|
this.splitMain.Panel2.Controls.Add(this.splitRight);
|
||||||
this.btnCleanSqlScript.Margin = new System.Windows.Forms.Padding(4);
|
this.splitMain.Size = new System.Drawing.Size(980, 620);
|
||||||
this.btnCleanSqlScript.Name = "btnCleanSqlScript";
|
this.splitMain.SplitterDistance = 240;
|
||||||
this.btnCleanSqlScript.Size = new System.Drawing.Size(100, 28);
|
this.splitMain.TabIndex = 0;
|
||||||
this.btnCleanSqlScript.TabIndex = 2;
|
|
||||||
this.btnCleanSqlScript.Text = "Очистить";
|
|
||||||
this.btnCleanSqlScript.UseVisualStyleBackColor = true;
|
|
||||||
this.btnCleanSqlScript.Click += new System.EventHandler(this.btnCleanSqlScript_Click);
|
|
||||||
//
|
//
|
||||||
// btnExecuteSqlScript
|
// listTemplates
|
||||||
//
|
//
|
||||||
this.btnExecuteSqlScript.Location = new System.Drawing.Point(128, 15);
|
this.listTemplates.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
this.btnExecuteSqlScript.Margin = new System.Windows.Forms.Padding(4);
|
this.listTemplates.Font = new System.Drawing.Font("Segoe UI", 10F);
|
||||||
this.btnExecuteSqlScript.Name = "btnExecuteSqlScript";
|
this.listTemplates.FormattingEnabled = true;
|
||||||
this.btnExecuteSqlScript.Size = new System.Drawing.Size(100, 28);
|
this.listTemplates.ItemHeight = 23;
|
||||||
this.btnExecuteSqlScript.TabIndex = 3;
|
this.listTemplates.Location = new System.Drawing.Point(0, 28);
|
||||||
this.btnExecuteSqlScript.Text = "Выполнить";
|
this.listTemplates.Name = "listTemplates";
|
||||||
this.btnExecuteSqlScript.UseVisualStyleBackColor = true;
|
this.listTemplates.Size = new System.Drawing.Size(240, 592);
|
||||||
this.btnExecuteSqlScript.Click += new System.EventHandler(this.btnExecuteSqlScript_Click);
|
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
|
// btnExecuteSelectSqlScript
|
||||||
//
|
//
|
||||||
this.btnExecuteSelectSqlScript.Location = new System.Drawing.Point(254, 15);
|
this.btnExecuteSelectSqlScript.Location = new System.Drawing.Point(232, 36);
|
||||||
this.btnExecuteSelectSqlScript.Margin = new System.Windows.Forms.Padding(4);
|
|
||||||
this.btnExecuteSelectSqlScript.Name = "btnExecuteSelectSqlScript";
|
this.btnExecuteSelectSqlScript.Name = "btnExecuteSelectSqlScript";
|
||||||
this.btnExecuteSelectSqlScript.Size = new System.Drawing.Size(182, 28);
|
this.btnExecuteSelectSqlScript.Size = new System.Drawing.Size(182, 28);
|
||||||
this.btnExecuteSelectSqlScript.TabIndex = 4;
|
this.btnExecuteSelectSqlScript.TabIndex = 4;
|
||||||
this.btnExecuteSelectSqlScript.Text = "Выполнить SELECT";
|
this.btnExecuteSelectSqlScript.Text = "Выполнить SELECT";
|
||||||
this.btnExecuteSelectSqlScript.UseVisualStyleBackColor = true;
|
|
||||||
this.btnExecuteSelectSqlScript.Click += new System.EventHandler(this.btnExecuteSelectSqlScript_Click);
|
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)));
|
||||||
|
this.lblDbPath.Location = new System.Drawing.Point(100, 8);
|
||||||
|
this.lblDbPath.Name = "lblDbPath";
|
||||||
|
this.lblDbPath.Size = new System.Drawing.Size(620, 20);
|
||||||
|
this.lblDbPath.TabIndex = 1;
|
||||||
|
this.lblDbPath.Text = "db";
|
||||||
|
//
|
||||||
|
// label1
|
||||||
|
//
|
||||||
|
this.label1.AutoSize = true;
|
||||||
|
this.label1.Location = new System.Drawing.Point(8, 8);
|
||||||
|
this.label1.Name = "label1";
|
||||||
|
this.label1.Size = new System.Drawing.Size(84, 16);
|
||||||
|
this.label1.TabIndex = 0;
|
||||||
|
this.label1.Text = "SQL / база:";
|
||||||
|
//
|
||||||
|
// RTXT_Sql
|
||||||
|
//
|
||||||
|
this.RTXT_Sql.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
this.RTXT_Sql.Font = new System.Drawing.Font("Consolas", 10F);
|
||||||
|
this.RTXT_Sql.Location = new System.Drawing.Point(0, 100);
|
||||||
|
this.RTXT_Sql.Name = "RTXT_Sql";
|
||||||
|
this.RTXT_Sql.Size = new System.Drawing.Size(736, 120);
|
||||||
|
this.RTXT_Sql.TabIndex = 3;
|
||||||
|
this.RTXT_Sql.Text = "";
|
||||||
|
//
|
||||||
|
// lblParams
|
||||||
|
//
|
||||||
|
this.lblParams.Dock = System.Windows.Forms.DockStyle.Top;
|
||||||
|
this.lblParams.Location = new System.Drawing.Point(0, 72);
|
||||||
|
this.lblParams.Name = "lblParams";
|
||||||
|
this.lblParams.Padding = new System.Windows.Forms.Padding(8, 4, 0, 0);
|
||||||
|
this.lblParams.Size = new System.Drawing.Size(736, 28);
|
||||||
|
this.lblParams.TabIndex = 1;
|
||||||
|
this.lblParams.Text = "Параметры (@name / {{name}})";
|
||||||
|
//
|
||||||
|
// panelParams
|
||||||
|
//
|
||||||
|
this.panelParams.AutoScroll = true;
|
||||||
|
this.panelParams.Dock = System.Windows.Forms.DockStyle.Bottom;
|
||||||
|
this.panelParams.Location = new System.Drawing.Point(0, 220);
|
||||||
|
this.panelParams.Name = "panelParams";
|
||||||
|
this.panelParams.Size = new System.Drawing.Size(736, 100);
|
||||||
|
this.panelParams.TabIndex = 2;
|
||||||
|
//
|
||||||
// dgvSelectCommand
|
// dgvSelectCommand
|
||||||
//
|
//
|
||||||
this.dgvSelectCommand.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
this.dgvSelectCommand.AllowUserToAddRows = false;
|
||||||
| System.Windows.Forms.AnchorStyles.Left)
|
this.dgvSelectCommand.AllowUserToDeleteRows = false;
|
||||||
| System.Windows.Forms.AnchorStyles.Right)));
|
this.dgvSelectCommand.AutoSizeColumnsMode = System.Windows.Forms.DataGridViewAutoSizeColumnsMode.DisplayedCells;
|
||||||
this.dgvSelectCommand.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
this.dgvSelectCommand.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
|
||||||
this.dgvSelectCommand.Location = new System.Drawing.Point(9, 195);
|
this.dgvSelectCommand.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
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(562, 150);
|
this.dgvSelectCommand.Size = new System.Drawing.Size(736, 268);
|
||||||
this.dgvSelectCommand.TabIndex = 5;
|
this.dgvSelectCommand.TabIndex = 0;
|
||||||
|
//
|
||||||
|
// 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(583, 351);
|
this.ClientSize = new System.Drawing.Size(980, 620);
|
||||||
this.Controls.Add(this.dgvSelectCommand);
|
this.Controls.Add(this.splitMain);
|
||||||
this.Controls.Add(this.btnExecuteSelectSqlScript);
|
this.MinimumSize = new System.Drawing.Size(800, 500);
|
||||||
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.Text = "FDebug";
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
|
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.RichTextBox RTXT_Sql;
|
private System.Windows.Forms.SplitContainer splitMain;
|
||||||
private System.Windows.Forms.Label label1;
|
private System.Windows.Forms.ListBox listTemplates;
|
||||||
private Elfisa.UI.Controls.ModernButton btnCleanSqlScript;
|
private System.Windows.Forms.Label lblTemplates;
|
||||||
private Elfisa.UI.Controls.ModernButton btnExecuteSqlScript;
|
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 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.Label lblParams;
|
||||||
|
private System.Windows.Forms.Panel panelParams;
|
||||||
private Elfisa.UI.Controls.ModernDataGridView dgvSelectCommand;
|
private Elfisa.UI.Controls.ModernDataGridView dgvSelectCommand;
|
||||||
|
private System.Windows.Forms.Label lblStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,81 +1,385 @@
|
|||||||
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;
|
using System.Text.RegularExpressions;
|
||||||
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 btnExecuteSqlScript_Click(object sender, EventArgs e)
|
private void FDebug_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
UiThemeHelper.ApplyToControlTree(this);
|
||||||
|
Text = "SQL-отладка";
|
||||||
string sqlScript = RTXT_Sql.Text;
|
lblDbPath.Text = AppConfig.SqliteDbPath;
|
||||||
|
RTXT_Sql.DetectUrls = false;
|
||||||
|
RTXT_Sql.AcceptsTab = true;
|
||||||
using (SQLiteConnection debugCon = new SQLiteConnection(connectionStringToLocalDB))
|
RTXT_Sql.TextChanged += RTXT_Sql_TextChanged;
|
||||||
|
BuildTemplates();
|
||||||
|
listTemplates.Items.Clear();
|
||||||
|
foreach (var template in _templates)
|
||||||
{
|
{
|
||||||
try
|
listTemplates.Items.Add(template.Title);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatus("Готово. Выберите шаблон или введите свой SQL.");
|
||||||
|
ApplySqlHighlight();
|
||||||
|
FormClosed += (_, __) =>
|
||||||
|
{
|
||||||
|
_highlightTimer.Stop();
|
||||||
|
_highlightTimer.Dispose();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RTXT_Sql_TextChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_highlighting)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_highlightTimer.Stop();
|
||||||
|
_highlightTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySqlHighlight()
|
||||||
|
{
|
||||||
|
if (_highlighting || RTXT_Sql == null || RTXT_Sql.IsDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_highlighting = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SqlSyntaxHighlighter.Apply(RTXT_Sql, ThemeManager.IsDarkProductivityActive);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_highlighting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
{
|
{
|
||||||
debugCon.Open();
|
Text = "Параметры не требуются. Можно писать {{name}} или @name в SQL.",
|
||||||
SQLiteCommand cmdDebug = new SQLiteCommand(sqlScript, debugCon);
|
AutoSize = true,
|
||||||
cmdDebug.ExecuteNonQuery();
|
MaximumSize = new Size(panelParams.Width - 20, 0),
|
||||||
}
|
Location = new Point(8, 8)
|
||||||
catch (Exception ex)
|
};
|
||||||
|
panelParams.Controls.Add(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int y = 8;
|
||||||
|
foreach (var name in parameters)
|
||||||
|
{
|
||||||
|
var lbl = new Label
|
||||||
{
|
{
|
||||||
MessageBox.Show($"При выполнении запроса возникла ошибка. Текст ошибки:\n{ex.ToString()}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
Text = "@" + name,
|
||||||
}
|
AutoSize = true,
|
||||||
finally
|
Location = new Point(8, y + 4)
|
||||||
|
};
|
||||||
|
var box = new TextBox
|
||||||
{
|
{
|
||||||
debugCon.Close();
|
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 = "";
|
RTXT_Sql.Text = string.Empty;
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
ExecuteSql(forceSelect: true);
|
||||||
|
}
|
||||||
|
|
||||||
string sqlScript = RTXT_Sql.Text;
|
private void ExecuteSql(bool forceSelect)
|
||||||
|
{
|
||||||
|
var sql = (RTXT_Sql.Text ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(sql))
|
||||||
|
{
|
||||||
|
SetStatus("Пустой SQL.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
using (SQLiteConnection debugSelectCon = new SQLiteConnection(connectionStringToLocalDB))
|
sql = ApplyMustachePlaceholders(sql);
|
||||||
|
bool isSelect = forceSelect || LooksLikeSelect(sql);
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
debugSelectCon.Open();
|
connection.Open();
|
||||||
SQLiteCommand cmdSelectSqlCommand = new SQLiteCommand(sqlScript, debugSelectCon);
|
using (var command = new SQLiteCommand(sql, connection))
|
||||||
SQLiteDataAdapter daSelectDataAdapter = new SQLiteDataAdapter(cmdSelectSqlCommand);
|
{
|
||||||
DataTable tableSelect = new DataTable();
|
BindParameters(command, sql);
|
||||||
daSelectDataAdapter.Fill(tableSelect);
|
|
||||||
dgvSelectCommand.DataSource = tableSelect;
|
if (isSelect)
|
||||||
|
{
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"Возникла ошибка при заполнении таблицы данными.\nТекст ошибки:\n{ex.ToString()}", "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
sw.Stop();
|
||||||
}
|
SetStatus("Ошибка: " + ex.Message);
|
||||||
finally
|
MessageBox.Show($"При выполнении запроса возникла ошибка.\n{ex}", "SQL-отладка", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
{
|
|
||||||
debugSelectCon.Close();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeSelect(string sql)
|
||||||
|
{
|
||||||
|
var trimmed = sql.TrimStart();
|
||||||
|
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}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(12, 56);
|
this.dgvConsignees.Location = new System.Drawing.Point(16, 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(409, 295);
|
this.dgvConsignees.Size = new System.Drawing.Size(640, 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(336, 361);
|
this.btnConfirmConsignee.Location = new System.Drawing.Point(567, 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(174, 361);
|
this.btnCloseThis.Location = new System.Drawing.Point(420, 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(434, 412);
|
this.ClientSize = new System.Drawing.Size(672, 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);
|
||||||
|
|||||||
@ -1,21 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Drawing;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using Электронная_Фармация.Classes;
|
|
||||||
using System.Data.SQLite;
|
using System.Data.SQLite;
|
||||||
|
using System.Windows.Forms;
|
||||||
using Электронная_Фармация;
|
using Электронная_Фармация;
|
||||||
|
using Электронная_Фармация.Classes;
|
||||||
|
|
||||||
namespace Электронная_Фармация.HelpForms
|
namespace Электронная_Фармация.HelpForms
|
||||||
{
|
{
|
||||||
public partial class HFConsignees : Form
|
public partial class HFConsignees : Form
|
||||||
{
|
{
|
||||||
public ElectroPharmacy ParentForm { get; set; }
|
public new ElectroPharmacy ParentForm { get; set; }
|
||||||
|
|
||||||
public HFConsignees()
|
public HFConsignees()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@ -31,28 +26,39 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
{
|
{
|
||||||
conConsigneesList.Open();
|
conConsigneesList.Open();
|
||||||
|
|
||||||
string qShowMeConsigneesList = "select ConsigneesName as [Грузополучатель] from Consignees order by ConsigneesUseAsDefault desc";
|
string qShowMeConsigneesList = @"
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
this.Close();
|
DialogResult = DialogResult.Cancel;
|
||||||
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnConfirmConsignee_Click(object sender, EventArgs e)
|
private void btnConfirmConsignee_Click(object sender, EventArgs e)
|
||||||
@ -63,7 +69,27 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
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();
|
||||||
|
|||||||
@ -43,22 +43,25 @@ 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();
|
||||||
|
|
||||||
SQLiteCommand cmdShowMeOrderStatus = new SQLiteCommand(qShowMeStatusOrder, conAddComment);
|
string orderState;
|
||||||
|
using (var cmdShowMeOrderStatus = new SQLiteCommand("select OrderState from Orders where Id_Order = @id", conAddComment))
|
||||||
string OrderState = cmdShowMeOrderStatus.ExecuteScalar().ToString();
|
|
||||||
|
|
||||||
if (OrderState == "НОВЫЙ")
|
|
||||||
{
|
{
|
||||||
SQLiteCommand cmdAddComment = new SQLiteCommand(qAddComment, conAddComment);
|
cmdShowMeOrderStatus.Parameters.AddWithValue("@id", _idOrder);
|
||||||
cmdAddComment.ExecuteNonQuery();
|
orderState = Convert.ToString(cmdShowMeOrderStatus.ExecuteScalar()) ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderState == "НОВЫЙ")
|
||||||
|
{
|
||||||
|
using (var cmdAddComment = new SQLiteCommand("update Orders set Comment = @comment where Id_Order = @id", conAddComment))
|
||||||
|
{
|
||||||
|
cmdAddComment.Parameters.AddWithValue("@comment", rtxtComment.Text);
|
||||||
|
cmdAddComment.Parameters.AddWithValue("@id", _idOrder);
|
||||||
|
cmdAddComment.ExecuteNonQuery();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@ -32,19 +32,64 @@
|
|||||||
this.dataGridView1 = new Elfisa.UI.Controls.ModernDataGridView();
|
this.dataGridView1 = new Elfisa.UI.Controls.ModernDataGridView();
|
||||||
this.DataSetPriceList = new System.Data.DataSet();
|
this.DataSetPriceList = new System.Data.DataSet();
|
||||||
this.btnCloseThis = new Elfisa.UI.Controls.ModernButton();
|
this.btnCloseThis = new Elfisa.UI.Controls.ModernButton();
|
||||||
|
this.lblTitle = new System.Windows.Forms.Label();
|
||||||
|
this.lblProgress = new System.Windows.Forms.Label();
|
||||||
|
this.progressDownload = new System.Windows.Forms.ProgressBar();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).BeginInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).BeginInit();
|
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).BeginInit();
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
|
// lblTitle
|
||||||
|
//
|
||||||
|
this.lblTitle.AutoSize = true;
|
||||||
|
this.lblTitle.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.lblTitle.Location = new System.Drawing.Point(22, 14);
|
||||||
|
this.lblTitle.Name = "lblTitle";
|
||||||
|
this.lblTitle.Size = new System.Drawing.Size(220, 28);
|
||||||
|
this.lblTitle.TabIndex = 5;
|
||||||
|
this.lblTitle.Text = "Загрузка прайс-листа";
|
||||||
|
//
|
||||||
|
// progressDownload
|
||||||
|
//
|
||||||
|
this.progressDownload.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||||
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.progressDownload.Location = new System.Drawing.Point(25, 52);
|
||||||
|
this.progressDownload.Name = "progressDownload";
|
||||||
|
this.progressDownload.Size = new System.Drawing.Size(630, 22);
|
||||||
|
this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Continuous;
|
||||||
|
this.progressDownload.Maximum = 100;
|
||||||
|
this.progressDownload.TabIndex = 6;
|
||||||
|
//
|
||||||
|
// lblProgress
|
||||||
|
//
|
||||||
|
this.lblProgress.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
|
||||||
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.lblProgress.Font = new System.Drawing.Font("Segoe UI", 9.75F);
|
||||||
|
this.lblProgress.Location = new System.Drawing.Point(25, 82);
|
||||||
|
this.lblProgress.Name = "lblProgress";
|
||||||
|
this.lblProgress.Size = new System.Drawing.Size(630, 40);
|
||||||
|
this.lblProgress.TabIndex = 7;
|
||||||
|
this.lblProgress.Text = "Подготовка...";
|
||||||
|
this.lblProgress.UseMnemonic = false;
|
||||||
|
//
|
||||||
// rtxtDebug
|
// rtxtDebug
|
||||||
//
|
//
|
||||||
this.rtxtDebug.BackColor = System.Drawing.SystemColors.Control;
|
this.rtxtDebug.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
|
||||||
this.rtxtDebug.BorderStyle = System.Windows.Forms.BorderStyle.None;
|
| System.Windows.Forms.AnchorStyles.Left)
|
||||||
this.rtxtDebug.Location = new System.Drawing.Point(25, 12);
|
| System.Windows.Forms.AnchorStyles.Right)));
|
||||||
|
this.rtxtDebug.BackColor = System.Drawing.SystemColors.Window;
|
||||||
|
this.rtxtDebug.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.rtxtDebug.DetectUrls = false;
|
||||||
|
this.rtxtDebug.Font = new System.Drawing.Font("Consolas", 9F);
|
||||||
|
this.rtxtDebug.HideSelection = false;
|
||||||
|
this.rtxtDebug.Location = new System.Drawing.Point(25, 128);
|
||||||
this.rtxtDebug.Name = "rtxtDebug";
|
this.rtxtDebug.Name = "rtxtDebug";
|
||||||
this.rtxtDebug.Size = new System.Drawing.Size(630, 319);
|
this.rtxtDebug.ReadOnly = true;
|
||||||
|
this.rtxtDebug.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Vertical;
|
||||||
|
this.rtxtDebug.Size = new System.Drawing.Size(630, 243);
|
||||||
this.rtxtDebug.TabIndex = 0;
|
this.rtxtDebug.TabIndex = 0;
|
||||||
this.rtxtDebug.Text = "";
|
this.rtxtDebug.Text = "";
|
||||||
|
this.rtxtDebug.WordWrap = true;
|
||||||
//
|
//
|
||||||
// dataGridView1
|
// dataGridView1
|
||||||
//
|
//
|
||||||
@ -63,8 +108,9 @@
|
|||||||
//
|
//
|
||||||
// btnCloseThis
|
// btnCloseThis
|
||||||
//
|
//
|
||||||
|
this.btnCloseThis.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
|
||||||
this.btnCloseThis.Font = new System.Drawing.Font("Segoe UI", 11.25F);
|
this.btnCloseThis.Font = new System.Drawing.Font("Segoe UI", 11.25F);
|
||||||
this.btnCloseThis.Location = new System.Drawing.Point(555, 337);
|
this.btnCloseThis.Location = new System.Drawing.Point(555, 381);
|
||||||
this.btnCloseThis.Margin = new System.Windows.Forms.Padding(4);
|
this.btnCloseThis.Margin = new System.Windows.Forms.Padding(4);
|
||||||
this.btnCloseThis.Name = "btnCloseThis";
|
this.btnCloseThis.Name = "btnCloseThis";
|
||||||
this.btnCloseThis.Size = new System.Drawing.Size(100, 41);
|
this.btnCloseThis.Size = new System.Drawing.Size(100, 41);
|
||||||
@ -78,12 +124,16 @@
|
|||||||
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.BackColor = System.Drawing.SystemColors.Control;
|
this.BackColor = System.Drawing.SystemColors.Control;
|
||||||
this.ClientSize = new System.Drawing.Size(684, 396);
|
this.ClientSize = new System.Drawing.Size(684, 440);
|
||||||
|
this.Controls.Add(this.lblProgress);
|
||||||
|
this.Controls.Add(this.progressDownload);
|
||||||
|
this.Controls.Add(this.lblTitle);
|
||||||
this.Controls.Add(this.btnCloseThis);
|
this.Controls.Add(this.btnCloseThis);
|
||||||
this.Controls.Add(this.dataGridView1);
|
this.Controls.Add(this.dataGridView1);
|
||||||
this.Controls.Add(this.rtxtDebug);
|
this.Controls.Add(this.rtxtDebug);
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
|
||||||
this.MaximizeBox = false;
|
this.MaximizeBox = true;
|
||||||
|
this.MinimumSize = new System.Drawing.Size(640, 420);
|
||||||
this.MinimizeBox = false;
|
this.MinimizeBox = false;
|
||||||
this.Name = "HF_DownloadDataFromServer";
|
this.Name = "HF_DownloadDataFromServer";
|
||||||
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
@ -93,7 +143,7 @@
|
|||||||
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
|
((System.ComponentModel.ISupportInitialize)(this.dataGridView1)).EndInit();
|
||||||
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).EndInit();
|
((System.ComponentModel.ISupportInitialize)(this.DataSetPriceList)).EndInit();
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
|
this.PerformLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -102,5 +152,8 @@
|
|||||||
private Elfisa.UI.Controls.ModernDataGridView dataGridView1;
|
private Elfisa.UI.Controls.ModernDataGridView dataGridView1;
|
||||||
private System.Data.DataSet DataSetPriceList;
|
private System.Data.DataSet DataSetPriceList;
|
||||||
private Elfisa.UI.Controls.ModernButton btnCloseThis;
|
private Elfisa.UI.Controls.ModernButton btnCloseThis;
|
||||||
|
private System.Windows.Forms.Label lblTitle;
|
||||||
|
private System.Windows.Forms.Label lblProgress;
|
||||||
|
private System.Windows.Forms.ProgressBar progressDownload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Data.SQLite;
|
using System.Data.SQLite;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
//using System.Net.Http.json;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using Электронная_Фармация.Classes;
|
using Электронная_Фармация.Classes;
|
||||||
@ -20,203 +14,471 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
{
|
{
|
||||||
public partial class HF_DownloadDataFromServer : Form
|
public partial class HF_DownloadDataFromServer : Form
|
||||||
{
|
{
|
||||||
|
// Доли общего прогресса 0..100.
|
||||||
|
private const int DownloadPhaseEnd = 45;
|
||||||
|
private const int BuildPhaseEnd = 55;
|
||||||
|
private const int SavePhaseEnd = 100;
|
||||||
|
|
||||||
public HF_DownloadDataFromServer()
|
public HF_DownloadDataFromServer()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
string login = Settings.Default.stringLogin.ToString();
|
// Чтобы при обновлении прайса не терять корзины других аптек,
|
||||||
string password = Settings.Default.stringPassword.ToString();
|
// чистим TempOrderItems только для активного грузополучателя.
|
||||||
|
public string ActiveConsigneeName { get; set; } = string.Empty;
|
||||||
|
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
private readonly string _login = Settings.Default.stringLogin ?? string.Empty;
|
||||||
|
private readonly string _password = Settings.Default.stringPassword ?? string.Empty;
|
||||||
|
private readonly string _connectionStringToLocalDb = AppConfig.SqliteConnectionString;
|
||||||
|
private bool _isDownloading;
|
||||||
|
|
||||||
private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e)
|
private async void HF_DownloadDataFromServer_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
UiThemeHelper.ApplyToControlTree(this);
|
UiThemeHelper.ApplyToControlTree(this);
|
||||||
|
ConfigureLogView();
|
||||||
btnCloseThis.Enabled = false;
|
btnCloseThis.Enabled = false;
|
||||||
rtxtDebug.Visible = false;
|
|
||||||
dataGridView1.Visible = false;
|
dataGridView1.Visible = false;
|
||||||
|
rtxtDebug.Visible = true;
|
||||||
|
rtxtDebug.ReadOnly = true;
|
||||||
|
_isDownloading = true;
|
||||||
|
SetOverallProgress(0, "Подключение к серверу...");
|
||||||
|
|
||||||
var client = new ApiClient();
|
var client = new ApiClient();
|
||||||
|
AppendStatus($"Сервер: {client.BaseUrl}");
|
||||||
|
AppendStatus($"Лог: {AppDebugLog.CurrentLogFilePath}");
|
||||||
|
AppDebugLog.Info("Download", $"Старт загрузки прайса. Сервер={client.BaseUrl}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(Settings.Default.stringToken))
|
SetOverallProgress(1, "Авторизация...");
|
||||||
|
await EnsureAuthenticatedAsync(client);
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
client.SetToken(Settings.Default.stringToken);
|
AppendStatus("Загружаю грузополучателей с сервера...");
|
||||||
rtxtDebug.Text += "Используется сохранённый токен...\n";
|
var locations = await client.GetBuyerLocationsAsync();
|
||||||
|
var synced = ConsigneeHelper.SyncFromServer(locations);
|
||||||
|
AppendStatus(synced > 0
|
||||||
|
? $"Грузополучателей обновлено: {synced}"
|
||||||
|
: "Грузополучателей на сервере пока нет");
|
||||||
}
|
}
|
||||||
else
|
catch (Exception locEx)
|
||||||
{
|
{
|
||||||
rtxtDebug.Text += "Выполняю авторизацию...\n";
|
AppendStatus($"Грузополучатели не обновлены: {locEx.Message}");
|
||||||
var token = await client.LoginAsync(login, password);
|
AppDebugLog.Error("Download", "Не удалось синхронизировать грузополучателей", locEx);
|
||||||
Settings.Default.stringToken = token;
|
|
||||||
Settings.Default.Save();
|
|
||||||
rtxtDebug.Text += "Авторизация успешно пройдена\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rtxtDebug.Text += "\nЗагружаю сводный прайс для всех поставщиков...";
|
SetOverallProgress(3, "Загрузка прайса с сервера...");
|
||||||
var allSuppliersSummary = await client.GetPriceSummaryAsync(
|
AppendStatus("Загружаю сводный прайс...");
|
||||||
|
var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client);
|
||||||
|
int totalItems = allSuppliersSummary.Summary?.Count ?? 0;
|
||||||
|
|
||||||
|
// Наценки прайса/региона/покупателя считает сервер.
|
||||||
|
// Десктоп сохраняет цену из API как есть, без локального пересчёта.
|
||||||
|
AppendStatus("Цена берётся с сервера без локального пересчёта наценок");
|
||||||
|
|
||||||
|
AppendStatus($"Получено {totalItems} позиций");
|
||||||
|
SetOverallProgress(DownloadPhaseEnd, $"Получено {totalItems} / {totalItems} позиций (100%)");
|
||||||
|
|
||||||
|
SetOverallProgress(DownloadPhaseEnd, "Подготовка таблицы...");
|
||||||
|
var buildProgress = new Progress<CountProgress>(p =>
|
||||||
|
{
|
||||||
|
int phasePercent = MapPhasePercent(p.Current, p.Total, DownloadPhaseEnd, BuildPhaseEnd);
|
||||||
|
SetOverallProgress(
|
||||||
|
phasePercent,
|
||||||
|
$"Обработка: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)");
|
||||||
|
});
|
||||||
|
var tablePrice = await Task.Run(() => BuildPriceTable(allSuppliersSummary, buildProgress));
|
||||||
|
|
||||||
|
SetOverallProgress(BuildPhaseEnd, "Сохранение в локальную базу...");
|
||||||
|
var saveProgress = new Progress<CountProgress>(p =>
|
||||||
|
{
|
||||||
|
int phasePercent = MapPhasePercent(p.Current, p.Total, BuildPhaseEnd, SavePhaseEnd);
|
||||||
|
SetOverallProgress(
|
||||||
|
phasePercent,
|
||||||
|
$"Сохранение: {p.Current} / {p.Total} ({PercentOf(p.Current, p.Total)}%)");
|
||||||
|
});
|
||||||
|
await Task.Run(() => SavePriceTable(tablePrice, saveProgress));
|
||||||
|
|
||||||
|
SetOverallProgress(100, $"Готово: {tablePrice.Rows.Count} / {tablePrice.Rows.Count} (100%)");
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
AppendStatus($"Готово. Сохранено {tablePrice.Rows.Count} позиций в локальную базу.");
|
||||||
|
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
|
||||||
|
|
||||||
|
Settings.Default.LastPriceUpdateUtc = DateTime.UtcNow;
|
||||||
|
Settings.Default.LastPriceReminderUtc = DateTime.MinValue;
|
||||||
|
Settings.Default.Save();
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
SetOverallProgress(0, "Ошибка авторизации");
|
||||||
|
AppendStatus($"Ошибка авторизации: {ex.Message}");
|
||||||
|
AppDebugLog.Error("Download", "Ошибка авторизации при загрузке прайса", ex);
|
||||||
|
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
SetOverallProgress(0, "Ошибка сети");
|
||||||
|
AppendStatus($"Ошибка сети: {ex.Message}");
|
||||||
|
AppDebugLog.Error("Download", "Ошибка сети при загрузке прайса", ex);
|
||||||
|
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SetOverallProgress(0, "Ошибка загрузки");
|
||||||
|
AppendStatus($"Ошибка загрузки прайса: {ex.Message}");
|
||||||
|
AppDebugLog.Error("Download", "Неожиданная ошибка при загрузке прайса", ex);
|
||||||
|
ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isDownloading = false;
|
||||||
|
btnCloseThis.Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task EnsureAuthenticatedAsync(ApiClient client)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(Settings.Default.stringToken))
|
||||||
|
{
|
||||||
|
client.SetToken(Settings.Default.stringToken);
|
||||||
|
AppendStatus("Используется сохранённый токен...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoginAndSaveTokenAsync(client);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoginAndSaveTokenAsync(ApiClient client)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_login) || string.IsNullOrWhiteSpace(_password))
|
||||||
|
{
|
||||||
|
throw new UnauthorizedAccessException(
|
||||||
|
"Не заданы логин и пароль. Выполните авторизацию в меню «Регистрация».");
|
||||||
|
}
|
||||||
|
|
||||||
|
AppendStatus("Выполняю авторизацию...");
|
||||||
|
var token = await client.LoginAsync(_login, _password);
|
||||||
|
Settings.Default.stringToken = token;
|
||||||
|
Settings.Default.Save();
|
||||||
|
AppendStatus("Авторизация успешно пройдена");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<PriceSummaryResponse> GetFullPriceSummaryPagedAsync(ApiClient client)
|
||||||
|
{
|
||||||
|
var regionId = string.IsNullOrWhiteSpace(Settings.Default.RegionId)
|
||||||
|
? null
|
||||||
|
: Settings.Default.RegionId;
|
||||||
|
|
||||||
|
const int supplierPageLimit = 100000;
|
||||||
|
const int discoveryLimit = 50000;
|
||||||
|
|
||||||
|
var combined = new PriceSummaryResponse { Summary = new List<PriceSummaryItem>() };
|
||||||
|
var transferProgress = new Progress<HttpTransferProgress>(ReportTransferProgress);
|
||||||
|
|
||||||
|
var supplierIds = new List<string>(ConsigneeHelper.GetKnownSupplierIds());
|
||||||
|
if (supplierIds.Count == 0)
|
||||||
|
{
|
||||||
|
AppendStatus("Первая загрузка: определяю список поставщиков...");
|
||||||
|
SetOverallProgress(3, "Определение поставщиков...");
|
||||||
|
var discovery = await GetPriceSummaryWithAuthRetryAsync(
|
||||||
|
client,
|
||||||
supplierId: null,
|
supplierId: null,
|
||||||
regionId: string.IsNullOrWhiteSpace(Settings.Default.RegionId) ? null : Settings.Default.RegionId);
|
regionId: regionId,
|
||||||
rtxtDebug.Text += ($"✓ Получено {allSuppliersSummary.Summary.Count} позиций\n");
|
limit: discoveryLimit,
|
||||||
|
offset: 0,
|
||||||
|
transferProgress: transferProgress);
|
||||||
|
|
||||||
DataTable tablePrice = new DataTable();
|
if (discovery?.MarkupPercent.HasValue == true)
|
||||||
|
{
|
||||||
|
combined.MarkupPercent = discovery.MarkupPercent;
|
||||||
|
}
|
||||||
|
|
||||||
tablePrice.Columns.Add("supplier_price_id");
|
var discovered = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
tablePrice.Columns.Add("guid_es");
|
if (discovery?.Summary != null)
|
||||||
tablePrice.Columns.Add("es_code");
|
{
|
||||||
tablePrice.Columns.Add("supplier_id");
|
foreach (var item in discovery.Summary)
|
||||||
tablePrice.Columns.Add("DrugName");
|
|
||||||
tablePrice.Columns.Add("SupplierName");
|
|
||||||
tablePrice.Columns.Add("Price");
|
|
||||||
tablePrice.Columns.Add("Quantity");
|
|
||||||
|
|
||||||
tablePrice.Columns.Add(new DataColumn
|
|
||||||
{
|
|
||||||
ColumnName = "ExpiryPeriod",
|
|
||||||
DataType = typeof(string),
|
|
||||||
AllowDBNull = true
|
|
||||||
});
|
|
||||||
|
|
||||||
tablePrice.Columns.Add(new DataColumn
|
|
||||||
{
|
|
||||||
ColumnName = "Description",
|
|
||||||
DataType = typeof(string),
|
|
||||||
AllowDBNull = true
|
|
||||||
});
|
|
||||||
|
|
||||||
//DataColumn colExpiryPeriod = new DataColumn("ExpiryPeriod", typeof(DateTime));
|
|
||||||
//DataColumn colDescription = new DataColumn("Description", typeof(string));
|
|
||||||
|
|
||||||
//colExpiryPeriod.AllowDBNull = true;
|
|
||||||
//colDescription.AllowDBNull = true;
|
|
||||||
|
|
||||||
|
|
||||||
////tablePrice.Columns.Add(colExpiryPeriod);
|
|
||||||
//tablePrice.Columns.Add(colDescription);
|
|
||||||
|
|
||||||
|
|
||||||
tablePrice.Columns.Add("Zakaz");
|
|
||||||
tablePrice.Columns.Add("SummaZakaza");
|
|
||||||
|
|
||||||
|
|
||||||
#region полная таблица
|
|
||||||
//tablePrice.Columns.Add("GuidEs");
|
|
||||||
//tablePrice.Columns.Add("SupplierId");
|
|
||||||
//tablePrice.Columns.Add("SupplierName");
|
|
||||||
//tablePrice.Columns.Add("DrugName");
|
|
||||||
//tablePrice.Columns.Add("Inn");
|
|
||||||
//tablePrice.Columns.Add("CureForm");
|
|
||||||
//tablePrice.Columns.Add("Barcode");
|
|
||||||
//tablePrice.Columns.Add("Price");
|
|
||||||
//tablePrice.Columns.Add("Quantity");
|
|
||||||
//tablePrice.Columns.Add("RegionId");
|
|
||||||
//tablePrice.Columns.Add("RegionName");
|
|
||||||
//tablePrice.Columns.Add("LastPriceDate");
|
|
||||||
//tablePrice.Columns.Add("MatchMethod");
|
|
||||||
//tablePrice.Columns.Add("MatchConfidence");
|
|
||||||
//tablePrice.Columns.Add("TradeName");
|
|
||||||
//tablePrice.Columns.Add("Dosage");
|
|
||||||
//tablePrice.Columns.Add("RegistryPrice");
|
|
||||||
//tablePrice.Columns.Add("InstructionGuid");
|
|
||||||
//tablePrice.Columns.Add("Description");
|
|
||||||
//tablePrice.Columns.Add("StoringCondition");
|
|
||||||
//tablePrice.Columns.Add("ExpiryPeriod");
|
|
||||||
//tablePrice.Columns.Add("ProducerName");
|
|
||||||
//tablePrice.Columns.Add("RegistryDate");
|
|
||||||
//tablePrice.Columns.Add("EsCode");
|
|
||||||
//tablePrice.Columns.Add("RegistryStatus");
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
if (allSuppliersSummary.Summary != null)
|
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(item.SupplierId))
|
||||||
foreach (var item in allSuppliersSummary.Summary)
|
|
||||||
{
|
{
|
||||||
|
discovered.Add(item.SupplierId.Trim());
|
||||||
DataRow row = tablePrice.NewRow();
|
|
||||||
|
|
||||||
row["supplier_price_id"] = item.SupplierPriceID.ToString();
|
|
||||||
row["guid_es"] = item.GuidEs.ToString();
|
|
||||||
row["es_code"] = item.EsCode.ToString();
|
|
||||||
row["supplier_id"] = item.SupplierId.ToString();
|
|
||||||
row["DrugName"] = item.DrugName.ToString();
|
|
||||||
row["SupplierName"] = item.SupplierName.ToString();
|
|
||||||
row["Price"] = item.Price.ToString();
|
|
||||||
row["Quantity"] = item.Quantity.ToString();
|
|
||||||
|
|
||||||
if (item.ExpiryPeriod == null)
|
|
||||||
{ row["ExpiryPeriod"] = DBNull.Value; }
|
|
||||||
else { row["ExpiryPeriod"] = item.ExpiryPeriod.ToString(); }
|
|
||||||
;
|
|
||||||
|
|
||||||
if (item.Description == null)
|
|
||||||
{ row["Description"] = DBNull.Value; }
|
|
||||||
else { row["Description"] = item.Description.ToString(); }
|
|
||||||
;
|
|
||||||
|
|
||||||
row["Zakaz"] = string.Empty;
|
|
||||||
row["SummaZakaza"] = string.Empty;
|
|
||||||
|
|
||||||
tablePrice.Rows.Add(row);
|
|
||||||
|
|
||||||
// DataSetPriceList.Tables.Add(tablePrice);
|
|
||||||
// DataSetPriceList.WriteXml("PriceList.xml", XmlWriteMode.WriteSchema);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//SQLiteDataAdapter sqlite_data = new SQLiteDataAdapter();
|
|
||||||
//new SQLiteCommandBuilder(sqlite_data);
|
|
||||||
////sqlite_data.Fill(tablePrice);
|
|
||||||
//sqlite_data.Update(tablePrice);
|
|
||||||
//dataGridView1.DataSource = tablePrice;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#region сводный прайс и статистика
|
|
||||||
//// Шаг 3: Получение сводного прайса для конкретного поставщика
|
|
||||||
//if (allSuppliersSummary.Summary.Any())
|
|
||||||
//{
|
|
||||||
// var firstSupplierId = allSuppliersSummary.Summary
|
|
||||||
// .FirstOrDefault(s => !string.IsNullOrEmpty(s.SupplierId))?.SupplierId;
|
|
||||||
|
|
||||||
// if (!string.IsNullOrEmpty(firstSupplierId))
|
|
||||||
// {
|
|
||||||
// Console.WriteLine($"\nЗагружаю прайс для поставщика {firstSupplierId}...");
|
|
||||||
// var supplierSummary = await client.GetPriceSummaryAsync(supplierId: firstSupplierId);
|
|
||||||
// Console.WriteLine($"✓ Получено {supplierSummary.Summary.Count} позиций для поставщика\n");
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
//// Шаг 4: Статистика
|
|
||||||
//Console.WriteLine("\n=== Статистика ===");
|
|
||||||
//var withPrice = allSuppliersSummary.Summary.Where(s => s.Price.HasValue).ToList();
|
|
||||||
//var withQuantity = allSuppliersSummary.Summary.Where(s => s.Quantity.HasValue).ToList();
|
|
||||||
|
|
||||||
//Console.WriteLine($"Всего позиций: {allSuppliersSummary.Summary.Count}");
|
|
||||||
//Console.WriteLine($"С ценой: {withPrice.Count}");
|
|
||||||
//Console.WriteLine($"С количеством: {withQuantity.Count}");
|
|
||||||
|
|
||||||
|
|
||||||
//if (withPrice.Any())
|
|
||||||
//{
|
|
||||||
// var avgPrice = withPrice.Average(s => s.Price.Value);
|
|
||||||
// var minPrice = withPrice.Min(s => s.Price.Value);
|
|
||||||
// var maxPrice = withPrice.Max(s => s.Price.Value);
|
|
||||||
|
|
||||||
// Console.WriteLine($"Средняя цена: {avgPrice:F2} ₽");
|
|
||||||
// Console.WriteLine($"Минимальная цена: {minPrice:F2} ₽");
|
|
||||||
// Console.WriteLine($"Максимальная цена: {maxPrice:F2} ₽");
|
|
||||||
//}
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
string commandToDelete = "delete from [PriceList]";
|
supplierIds.AddRange(discovered);
|
||||||
string commandTODeleteTempOrder = "delete from [TempOrderItems]";
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
string commandToInsert = @"INSERT INTO [PriceList] (
|
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(
|
||||||
|
supplierProgressStart,
|
||||||
|
$"Поставщик {i + 1}/{totalSuppliers}...");
|
||||||
|
AppendStatus($"Загрузка поставщика {i + 1}/{totalSuppliers}: {supplierId}");
|
||||||
|
|
||||||
|
PriceSummaryResponse chunk;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
chunk = await GetPriceSummaryWithAuthRetryAsync(
|
||||||
|
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,
|
||||||
|
regionId: regionId,
|
||||||
|
limit: 5000,
|
||||||
|
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,
|
||||||
|
transferProgress: transferProgress);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
AppendStatus("Токен недействителен, повторная авторизация...");
|
||||||
|
SetOverallProgress(2, "Повторная авторизация...");
|
||||||
|
Settings.Default.stringToken = string.Empty;
|
||||||
|
Settings.Default.Save();
|
||||||
|
client.SetToken(null);
|
||||||
|
await LoginAndSaveTokenAsync(client);
|
||||||
|
return await client.GetPriceSummaryAsync(
|
||||||
|
supplierId: supplierId,
|
||||||
|
regionId: regionId,
|
||||||
|
limit: limit,
|
||||||
|
offset: offset,
|
||||||
|
transferProgress: transferProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReportTransferProgress(HttpTransferProgress transfer)
|
||||||
|
{
|
||||||
|
if (transfer.TotalBytes.HasValue && transfer.TotalBytes.Value > 0)
|
||||||
|
{
|
||||||
|
int phasePercent = MapPhasePercent(transfer.BytesReceived, transfer.TotalBytes.Value, 3, DownloadPhaseEnd);
|
||||||
|
SetOverallProgress(
|
||||||
|
phasePercent,
|
||||||
|
$"Скачивание: {FormatBytes(transfer.BytesReceived)} / {FormatBytes(transfer.TotalBytes.Value)} ({transfer.Percent}%)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetOverallProgress(
|
||||||
|
Math.Min(DownloadPhaseEnd - 1, 5),
|
||||||
|
$"Скачивание: получено {FormatBytes(transfer.BytesReceived)}...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DataTable BuildPriceTable(
|
||||||
|
PriceSummaryResponse allSuppliersSummary,
|
||||||
|
IProgress<CountProgress> progress = null)
|
||||||
|
{
|
||||||
|
var tablePrice = new DataTable();
|
||||||
|
tablePrice.Columns.Add("supplier_price_id");
|
||||||
|
tablePrice.Columns.Add("guid_es");
|
||||||
|
tablePrice.Columns.Add("es_code");
|
||||||
|
tablePrice.Columns.Add("supplier_id");
|
||||||
|
tablePrice.Columns.Add("DrugName");
|
||||||
|
tablePrice.Columns.Add("SupplierName");
|
||||||
|
tablePrice.Columns.Add("Price");
|
||||||
|
tablePrice.Columns.Add("Quantity");
|
||||||
|
tablePrice.Columns.Add(new DataColumn
|
||||||
|
{
|
||||||
|
ColumnName = "ExpiryPeriod",
|
||||||
|
DataType = typeof(string),
|
||||||
|
AllowDBNull = true
|
||||||
|
});
|
||||||
|
tablePrice.Columns.Add(new DataColumn
|
||||||
|
{
|
||||||
|
ColumnName = "Description",
|
||||||
|
DataType = typeof(string),
|
||||||
|
AllowDBNull = true
|
||||||
|
});
|
||||||
|
tablePrice.Columns.Add("Zakaz");
|
||||||
|
tablePrice.Columns.Add("SummaZakaza");
|
||||||
|
tablePrice.Columns.Add(new DataColumn
|
||||||
|
{
|
||||||
|
ColumnName = "TradeName",
|
||||||
|
DataType = typeof(string),
|
||||||
|
AllowDBNull = true
|
||||||
|
});
|
||||||
|
tablePrice.Columns.Add(new DataColumn
|
||||||
|
{
|
||||||
|
ColumnName = "Dosage",
|
||||||
|
DataType = typeof(string),
|
||||||
|
AllowDBNull = true
|
||||||
|
});
|
||||||
|
tablePrice.Columns.Add(new DataColumn
|
||||||
|
{
|
||||||
|
ColumnName = "MarkupPercent",
|
||||||
|
DataType = typeof(decimal),
|
||||||
|
AllowDBNull = true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (allSuppliersSummary?.Summary == null)
|
||||||
|
{
|
||||||
|
return tablePrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = allSuppliersSummary.Summary.Count;
|
||||||
|
int current = 0;
|
||||||
|
foreach (var item in allSuppliersSummary.Summary)
|
||||||
|
{
|
||||||
|
var row = tablePrice.NewRow();
|
||||||
|
row["supplier_price_id"] = item.SupplierPriceID ?? string.Empty;
|
||||||
|
row["guid_es"] = item.GuidEs ?? string.Empty;
|
||||||
|
row["es_code"] = item.EsCode?.ToString() ?? "0";
|
||||||
|
row["supplier_id"] = item.SupplierId ?? string.Empty;
|
||||||
|
row["DrugName"] = item.DrugName ?? string.Empty;
|
||||||
|
row["SupplierName"] = item.SupplierName ?? string.Empty;
|
||||||
|
|
||||||
|
// Цена уже итоговая с сервера — локально не пересчитываем.
|
||||||
|
var serverPrice = item.Price ?? 0m;
|
||||||
|
row["Price"] = serverPrice.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
row["Quantity"] = item.Quantity?.ToString() ?? string.Empty;
|
||||||
|
row["ExpiryPeriod"] = item.ExpiryPeriod == null ? (object)DBNull.Value : item.ExpiryPeriod;
|
||||||
|
row["Description"] = item.Description == null ? (object)DBNull.Value : item.Description;
|
||||||
|
row["Zakaz"] = string.Empty;
|
||||||
|
row["SummaZakaza"] = string.Empty;
|
||||||
|
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["MarkupPercent"] = item.MarkupPercent.HasValue && item.MarkupPercent.Value > 0m
|
||||||
|
? item.MarkupPercent.Value
|
||||||
|
: 0m;
|
||||||
|
tablePrice.Rows.Add(row);
|
||||||
|
|
||||||
|
current++;
|
||||||
|
if (progress != null && (current % 500 == 0 || current == total))
|
||||||
|
{
|
||||||
|
progress.Report(new CountProgress(current, total));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tablePrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavePriceTable(DataTable tablePrice, IProgress<CountProgress> progress = null)
|
||||||
|
{
|
||||||
|
const string commandToDelete = "delete from [PriceList]";
|
||||||
|
var commandToDeleteTempOrder = string.IsNullOrWhiteSpace(ActiveConsigneeName)
|
||||||
|
? "delete from [TempOrderItems]"
|
||||||
|
: "delete from [TempOrderItems] where [ConsigneeName] = @consignee";
|
||||||
|
const string commandToInsert = @"INSERT INTO [PriceList] (
|
||||||
[guid_es],
|
[guid_es],
|
||||||
[es_code],
|
[es_code],
|
||||||
[supplier_id],
|
[supplier_id],
|
||||||
@ -228,7 +490,10 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
[Description],
|
[Description],
|
||||||
[Zakaz],
|
[Zakaz],
|
||||||
[SummaZakaza],
|
[SummaZakaza],
|
||||||
[supplier_price_id]
|
[supplier_price_id],
|
||||||
|
[TradeName],
|
||||||
|
[Dosage],
|
||||||
|
[MarkupPercent]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -243,74 +508,253 @@ VALUES
|
|||||||
@Description,
|
@Description,
|
||||||
@Zakaz,
|
@Zakaz,
|
||||||
@SummaZakaza,
|
@SummaZakaza,
|
||||||
@supplier_price_id
|
@supplier_price_id,
|
||||||
)
|
@TradeName,
|
||||||
";
|
@Dosage,
|
||||||
|
@MarkupPercent
|
||||||
|
)";
|
||||||
|
|
||||||
using (SQLiteConnection sqliteCon = new SQLiteConnection(connectionStringToLocalDB))
|
int total = tablePrice.Rows.Count;
|
||||||
|
using (var sqliteCon = new SQLiteConnection(_connectionStringToLocalDb))
|
||||||
|
{
|
||||||
|
sqliteCon.Open();
|
||||||
|
|
||||||
|
using (var cmdDeleteBeforeInsert = new SQLiteCommand(commandToDelete, sqliteCon))
|
||||||
|
using (var cmdDeleteTempOrder = new SQLiteCommand(commandToDeleteTempOrder, sqliteCon))
|
||||||
|
{
|
||||||
|
cmdDeleteBeforeInsert.ExecuteNonQuery();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(ActiveConsigneeName) &&
|
||||||
|
commandToDeleteTempOrder.Contains("@consignee"))
|
||||||
|
{
|
||||||
|
cmdDeleteTempOrder.Parameters.AddWithValue("@consignee", ActiveConsigneeName.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdDeleteTempOrder.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
bool hasTradeName = tablePrice.Columns.Contains("TradeName");
|
||||||
|
bool hasDosage = tablePrice.Columns.Contains("Dosage");
|
||||||
|
bool hasMarkup = tablePrice.Columns.Contains("MarkupPercent");
|
||||||
|
|
||||||
|
foreach (DataRow row in tablePrice.Rows)
|
||||||
|
{
|
||||||
|
pGuidEs.Value = row["guid_es"] ?? DBNull.Value;
|
||||||
|
pEsCode.Value = row["es_code"] ?? DBNull.Value;
|
||||||
|
pSupplierId.Value = row["supplier_id"] ?? DBNull.Value;
|
||||||
|
pDrugName.Value = row["DrugName"] ?? DBNull.Value;
|
||||||
|
pSupplierName.Value = row["SupplierName"] ?? DBNull.Value;
|
||||||
|
pPrice.Value = row["Price"] ?? DBNull.Value;
|
||||||
|
pQuantity.Value = row["Quantity"] ?? DBNull.Value;
|
||||||
|
pExpiry.Value = row["ExpiryPeriod"] ?? DBNull.Value;
|
||||||
|
pDescription.Value = row["Description"] ?? DBNull.Value;
|
||||||
|
pZakaz.Value = row["Zakaz"] ?? DBNull.Value;
|
||||||
|
pSumma.Value = row["SummaZakaza"] ?? DBNull.Value;
|
||||||
|
pSupplierPriceId.Value = row["supplier_price_id"] ?? DBNull.Value;
|
||||||
|
pTradeName.Value = hasTradeName ? (row["TradeName"] ?? DBNull.Value) : DBNull.Value;
|
||||||
|
pDosage.Value = hasDosage ? (row["Dosage"] ?? DBNull.Value) : DBNull.Value;
|
||||||
|
pMarkup.Value = hasMarkup ? (row["MarkupPercent"] ?? DBNull.Value) : DBNull.Value;
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
|
||||||
|
saved++;
|
||||||
|
if (progress != null && (saved % 250 == 0 || saved == total))
|
||||||
{
|
{
|
||||||
sqliteCon.Open();
|
progress.Report(new CountProgress(saved, total));
|
||||||
|
|
||||||
SQLiteCommand cmdDeleteBeforeInsert = new SQLiteCommand(commandToDelete, sqliteCon);
|
|
||||||
SQLiteCommand cmdDeleteTempOrder = new SQLiteCommand(commandTODeleteTempOrder, sqliteCon);
|
|
||||||
|
|
||||||
cmdDeleteBeforeInsert.ExecuteNonQuery();
|
|
||||||
cmdDeleteTempOrder.ExecuteNonQuery();
|
|
||||||
|
|
||||||
using (var transaction = sqliteCon.BeginTransaction())
|
|
||||||
{
|
|
||||||
foreach (DataRow row in tablePrice.Rows)
|
|
||||||
{
|
|
||||||
using (SQLiteCommand cmd = new SQLiteCommand(commandToInsert, sqliteCon, transaction))
|
|
||||||
{
|
|
||||||
cmd.Parameters.AddWithValue("@guid_es", row["guid_es"]);
|
|
||||||
cmd.Parameters.AddWithValue("@es_code", row["es_code"]);
|
|
||||||
cmd.Parameters.AddWithValue("@supplier_id", row["supplier_id"]);
|
|
||||||
cmd.Parameters.AddWithValue("@DrugName", row["DrugName"]);
|
|
||||||
cmd.Parameters.AddWithValue("@SupplierName", row["SupplierName"]);
|
|
||||||
cmd.Parameters.AddWithValue("@Price", row["Price"]);
|
|
||||||
cmd.Parameters.AddWithValue("@Quantity", row["Quantity"]);
|
|
||||||
cmd.Parameters.AddWithValue("@ExpiryPeriod", row["ExpiryPeriod"]);
|
|
||||||
cmd.Parameters.AddWithValue("@Description", row["Description"]);
|
|
||||||
cmd.Parameters.AddWithValue("@Zakaz", row["Zakaz"]);
|
|
||||||
cmd.Parameters.AddWithValue("@SummaZakaza", row["SummaZakaza"]);
|
|
||||||
cmd.Parameters.AddWithValue("@supplier_price_id", row["supplier_price_id"]);
|
|
||||||
cmd.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
transaction.Commit();
|
|
||||||
}
|
|
||||||
sqliteCon.Close();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DialogResult = DialogResult.OK;
|
transaction.Commit();
|
||||||
ToastNotification.ShowSuccess($"Прайс обновлён: {tablePrice.Rows.Count} позиций");
|
}
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
}
|
||||||
|
|
||||||
|
private static int MapPhasePercent(long current, long total, int phaseStart, int phaseEnd)
|
||||||
|
{
|
||||||
|
if (total <= 0)
|
||||||
{
|
{
|
||||||
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
|
return phaseStart;
|
||||||
}
|
|
||||||
catch (HttpRequestException ex)
|
|
||||||
{
|
|
||||||
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
ToastNotification.ShowError($"Ошибка загрузки прайса: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
btnCloseThis.Enabled = true;
|
double ratio = Math.Max(0.0, Math.Min(1.0, (double)current / total));
|
||||||
|
return phaseStart + (int)((phaseEnd - phaseStart) * ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PercentOf(long current, long total)
|
||||||
|
{
|
||||||
|
if (total <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)Math.Max(0, Math.Min(100, (100.0 * current) / total));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatBytes(long bytes)
|
||||||
|
{
|
||||||
|
if (bytes < 1024)
|
||||||
|
{
|
||||||
|
return $"{bytes} Б";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes < 1024 * 1024)
|
||||||
|
{
|
||||||
|
return $"{bytes / 1024.0:0.0} КБ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{bytes / (1024.0 * 1024.0):0.00} МБ";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetOverallProgress(int percent, string text)
|
||||||
|
{
|
||||||
|
if (IsDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
BeginInvoke(new Action<int, string>(SetOverallProgress), percent, text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressDownload.Style != ProgressBarStyle.Continuous)
|
||||||
|
{
|
||||||
|
progressDownload.Style = ProgressBarStyle.Continuous;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressDownload.Minimum = 0;
|
||||||
|
progressDownload.Maximum = 100;
|
||||||
|
progressDownload.Value = Math.Max(0, Math.Min(100, percent));
|
||||||
|
lblProgress.Text = FormatProgressText(percent, text);
|
||||||
|
lblProgress.Refresh();
|
||||||
|
progressDownload.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigureLogView()
|
||||||
|
{
|
||||||
|
rtxtDebug.ReadOnly = true;
|
||||||
|
rtxtDebug.WordWrap = true;
|
||||||
|
rtxtDebug.ScrollBars = RichTextBoxScrollBars.Vertical;
|
||||||
|
rtxtDebug.HideSelection = false;
|
||||||
|
rtxtDebug.DetectUrls = false;
|
||||||
|
rtxtDebug.BorderStyle = BorderStyle.FixedSingle;
|
||||||
|
if (rtxtDebug.Font.Name != "Consolas")
|
||||||
|
{
|
||||||
|
rtxtDebug.Font = new Font("Consolas", 9f);
|
||||||
|
}
|
||||||
|
|
||||||
|
lblProgress.AutoEllipsis = false;
|
||||||
|
lblProgress.UseMnemonic = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatProgressText(int percent, string text)
|
||||||
|
{
|
||||||
|
return WrapLongText($"{Math.Max(0, Math.Min(100, percent))}% — {text}", 78, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WrapLongText(string text, int maxLineLength, int maxLines)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(text) || text.Length <= maxLineLength)
|
||||||
|
{
|
||||||
|
return text ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = new List<string>();
|
||||||
|
var remaining = text.Trim();
|
||||||
|
while (!string.IsNullOrEmpty(remaining) && lines.Count < maxLines)
|
||||||
|
{
|
||||||
|
if (remaining.Length <= maxLineLength)
|
||||||
|
{
|
||||||
|
lines.Add(remaining);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var breakIndex = remaining.LastIndexOf(' ', Math.Min(maxLineLength, remaining.Length - 1));
|
||||||
|
if (breakIndex <= 0)
|
||||||
|
{
|
||||||
|
breakIndex = maxLineLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.Add(remaining.Substring(0, breakIndex).TrimEnd());
|
||||||
|
remaining = remaining.Substring(breakIndex).TrimStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(remaining) && lines.Count >= maxLines)
|
||||||
|
{
|
||||||
|
var lastLine = lines[lines.Count - 1];
|
||||||
|
if (lastLine.Length > maxLineLength - 3)
|
||||||
|
{
|
||||||
|
lastLine = lastLine.Substring(0, Math.Max(0, maxLineLength - 3)).TrimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
lines[lines.Count - 1] = lastLine + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(Environment.NewLine, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendStatus(string message)
|
||||||
|
{
|
||||||
|
if (InvokeRequired)
|
||||||
|
{
|
||||||
|
BeginInvoke(new Action<string>(AppendStatus), message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapped = WrapLongText(message ?? string.Empty, 96, int.MaxValue);
|
||||||
|
rtxtDebug.AppendText(wrapped + Environment.NewLine);
|
||||||
|
rtxtDebug.SelectionStart = rtxtDebug.TextLength;
|
||||||
|
rtxtDebug.SelectionLength = 0;
|
||||||
|
rtxtDebug.ScrollToCaret();
|
||||||
|
rtxtDebug.Refresh();
|
||||||
|
AppDebugLog.Info("Download", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e)
|
private void HF_DownloadDataFromServer_FormClosing(object sender, FormClosingEventArgs e)
|
||||||
{
|
{
|
||||||
this.Dispose();
|
if (_isDownloading)
|
||||||
this.Close();
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnCloseThis_Click(object sender, EventArgs e)
|
private void btnCloseThis_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
this.Close();
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CountProgress
|
||||||
|
{
|
||||||
|
public CountProgress(int current, int total)
|
||||||
|
{
|
||||||
|
Current = current;
|
||||||
|
Total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Current { get; }
|
||||||
|
public int Total { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
153
src/ElectronicPharmacy/HelpForms/HF_LocationId.Designer.cs
generated
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
namespace Электронная_Фармация.HelpForms
|
||||||
|
{
|
||||||
|
partial class HF_LocationId
|
||||||
|
{
|
||||||
|
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.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(460, 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(430, 36);
|
||||||
|
this.lblTitle.TabIndex = 0;
|
||||||
|
this.lblTitle.Text = "Location ID";
|
||||||
|
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(20, 70);
|
||||||
|
this.lblHint.Name = "lblHint";
|
||||||
|
this.lblHint.Size = new System.Drawing.Size(420, 48);
|
||||||
|
this.lblHint.TabIndex = 1;
|
||||||
|
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
|
||||||
|
//
|
||||||
|
this.lblLocationId.AutoSize = true;
|
||||||
|
this.lblLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||||
|
this.lblLocationId.Location = new System.Drawing.Point(20, 182);
|
||||||
|
this.lblLocationId.Name = "lblLocationId";
|
||||||
|
this.lblLocationId.Size = new System.Drawing.Size(108, 28);
|
||||||
|
this.lblLocationId.TabIndex = 4;
|
||||||
|
this.lblLocationId.Text = "Location ID";
|
||||||
|
//
|
||||||
|
// txtLocationId
|
||||||
|
//
|
||||||
|
this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
|
||||||
|
this.txtLocationId.Location = new System.Drawing.Point(24, 214);
|
||||||
|
this.txtLocationId.Name = "txtLocationId";
|
||||||
|
this.txtLocationId.Size = new System.Drawing.Size(412, 34);
|
||||||
|
this.txtLocationId.TabIndex = 5;
|
||||||
|
//
|
||||||
|
// btnCancel
|
||||||
|
//
|
||||||
|
this.btnCancel.Location = new System.Drawing.Point(24, 275);
|
||||||
|
this.btnCancel.Name = "btnCancel";
|
||||||
|
this.btnCancel.Size = new System.Drawing.Size(125, 40);
|
||||||
|
this.btnCancel.TabIndex = 6;
|
||||||
|
this.btnCancel.Text = "Отмена";
|
||||||
|
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
|
||||||
|
//
|
||||||
|
// btnSave
|
||||||
|
//
|
||||||
|
this.btnSave.Location = new System.Drawing.Point(221, 275);
|
||||||
|
this.btnSave.Name = "btnSave";
|
||||||
|
this.btnSave.Size = new System.Drawing.Size(215, 40);
|
||||||
|
this.btnSave.TabIndex = 7;
|
||||||
|
this.btnSave.Text = "Сохранить";
|
||||||
|
this.btnSave.Click += new System.EventHandler(this.btnSave_Click);
|
||||||
|
//
|
||||||
|
// HF_LocationId
|
||||||
|
//
|
||||||
|
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(460, 330);
|
||||||
|
this.Controls.Add(this.btnSave);
|
||||||
|
this.Controls.Add(this.btnCancel);
|
||||||
|
this.Controls.Add(this.txtLocationId);
|
||||||
|
this.Controls.Add(this.lblConsigneeAddress);
|
||||||
|
this.Controls.Add(this.lblConsigneeName);
|
||||||
|
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_LocationId";
|
||||||
|
this.ShowIcon = false;
|
||||||
|
this.ShowInTaskbar = false;
|
||||||
|
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
|
||||||
|
this.Load += new System.EventHandler(this.HF_LocationId_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 lblConsigneeName;
|
||||||
|
private System.Windows.Forms.Label lblConsigneeAddress;
|
||||||
|
private System.Windows.Forms.Label lblLocationId;
|
||||||
|
private Elfisa.UI.Controls.ModernTextBox txtLocationId;
|
||||||
|
private Elfisa.UI.Controls.ModernButton btnCancel;
|
||||||
|
private Elfisa.UI.Controls.ModernButton btnSave;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/ElectronicPharmacy/HelpForms/HF_LocationId.cs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Elfisa.UI.Helpers;
|
||||||
|
using Elfisa.UI.Theming;
|
||||||
|
using Электронная_Фармация.Classes;
|
||||||
|
using Электронная_Фармация.Properties;
|
||||||
|
|
||||||
|
namespace Электронная_Фармация.HelpForms
|
||||||
|
{
|
||||||
|
public partial class HF_LocationId : Form
|
||||||
|
{
|
||||||
|
private string _consigneeName = string.Empty;
|
||||||
|
private string _consigneeAddress = string.Empty;
|
||||||
|
|
||||||
|
public HF_LocationId()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Shown += (_, __) => WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Показывает диалог Location ID. Возвращает true, если значение сохранено.
|
||||||
|
/// </summary>
|
||||||
|
public static bool PromptAndSave(IWin32Window owner)
|
||||||
|
{
|
||||||
|
using (var form = new HF_LocationId())
|
||||||
|
{
|
||||||
|
UiThemeHelper.ApplyToControlTree(form);
|
||||||
|
return form.ShowDialog(owner) == DialogResult.OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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)
|
||||||
|
{
|
||||||
|
UiThemeHelper.ApplyToControlTree(this);
|
||||||
|
WindowChromeHelper.ApplyDialogChrome(this, panelHeader, lblTitle);
|
||||||
|
txtLocationId.Text = Settings.Default.LocationId ?? string.Empty;
|
||||||
|
txtLocationId.SelectAll();
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var locationId = (txtLocationId.Text ?? string.Empty).Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(locationId))
|
||||||
|
{
|
||||||
|
UiDialogs.ShowError("Укажите Location ID торговой точки.", "Location ID", this);
|
||||||
|
txtLocationId.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig.SetLocationId(locationId);
|
||||||
|
AppDebugLog.Info("Auth", $"LocationId сохранён: {locationId}");
|
||||||
|
DialogResult = DialogResult.OK;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void btnCancel_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
DialogResult = DialogResult.Cancel;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,10 +38,6 @@
|
|||||||
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.label4 = new System.Windows.Forms.Label();
|
|
||||||
this.txtLocationId = new Elfisa.UI.Controls.ModernTextBox();
|
|
||||||
this.label5 = new System.Windows.Forms.Label();
|
|
||||||
this.txtApiUrl = new Elfisa.UI.Controls.ModernTextBox();
|
|
||||||
this.panel1.SuspendLayout();
|
this.panel1.SuspendLayout();
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
@ -83,16 +79,16 @@
|
|||||||
//
|
//
|
||||||
// btnConfirmRegistration
|
// btnConfirmRegistration
|
||||||
//
|
//
|
||||||
this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 401);
|
this.btnConfirmRegistration.Location = new System.Drawing.Point(188, 244);
|
||||||
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;
|
||||||
this.btnConfirmRegistration.Text = "Зарегистрировать";
|
this.btnConfirmRegistration.Text = "Войти";
|
||||||
this.btnConfirmRegistration.Click += new System.EventHandler(this.btnConfirmRegistration_Click);
|
this.btnConfirmRegistration.Click += new System.EventHandler(this.btnConfirmRegistration_Click);
|
||||||
//
|
//
|
||||||
// btnCloseThis
|
// btnCloseThis
|
||||||
//
|
//
|
||||||
this.btnCloseThis.Location = new System.Drawing.Point(28, 401);
|
this.btnCloseThis.Location = new System.Drawing.Point(28, 244);
|
||||||
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;
|
||||||
@ -104,7 +100,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, 342);
|
this.checkTokenGained.Location = new System.Drawing.Point(28, 304);
|
||||||
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,51 +131,11 @@
|
|||||||
this.label3.Text = "АВТОРИЗАЦИЯ";
|
this.label3.Text = "АВТОРИЗАЦИЯ";
|
||||||
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
this.label3.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
|
||||||
//
|
//
|
||||||
// label4
|
|
||||||
//
|
|
||||||
this.label4.AutoSize = true;
|
|
||||||
this.label4.Font = new System.Drawing.Font("Segoe UI", 12F);
|
|
||||||
this.label4.Location = new System.Drawing.Point(23, 225);
|
|
||||||
this.label4.Name = "label4";
|
|
||||||
this.label4.Size = new System.Drawing.Size(108, 28);
|
|
||||||
this.label4.TabIndex = 9;
|
|
||||||
this.label4.Text = "Location ID";
|
|
||||||
//
|
|
||||||
// txtLocationId
|
|
||||||
//
|
|
||||||
this.txtLocationId.Font = new System.Drawing.Font("Segoe UI", 12F);
|
|
||||||
this.txtLocationId.Location = new System.Drawing.Point(28, 256);
|
|
||||||
this.txtLocationId.Name = "txtLocationId";
|
|
||||||
this.txtLocationId.Size = new System.Drawing.Size(375, 34);
|
|
||||||
this.txtLocationId.TabIndex = 10;
|
|
||||||
//
|
|
||||||
// label5
|
|
||||||
//
|
|
||||||
this.label5.AutoSize = true;
|
|
||||||
this.label5.Font = new System.Drawing.Font("Segoe UI", 12F);
|
|
||||||
this.label5.Location = new System.Drawing.Point(23, 296);
|
|
||||||
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, 296);
|
|
||||||
this.txtApiUrl.Name = "txtApiUrl";
|
|
||||||
this.txtApiUrl.Size = new System.Drawing.Size(271, 34);
|
|
||||||
this.txtApiUrl.TabIndex = 12;
|
|
||||||
//
|
|
||||||
// HF_Registration
|
// HF_Registration
|
||||||
//
|
//
|
||||||
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, 456);
|
this.ClientSize = new System.Drawing.Size(436, 300);
|
||||||
this.Controls.Add(this.txtApiUrl);
|
|
||||||
this.Controls.Add(this.label5);
|
|
||||||
this.Controls.Add(this.txtLocationId);
|
|
||||||
this.Controls.Add(this.label4);
|
|
||||||
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);
|
||||||
@ -199,7 +155,6 @@
|
|||||||
this.panel1.ResumeLayout(false);
|
this.panel1.ResumeLayout(false);
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
this.PerformLayout();
|
this.PerformLayout();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -213,9 +168,5 @@
|
|||||||
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 label4;
|
|
||||||
private Elfisa.UI.Controls.ModernTextBox txtLocationId;
|
|
||||||
private System.Windows.Forms.Label label5;
|
|
||||||
private Elfisa.UI.Controls.ModernTextBox txtApiUrl;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Data;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Forms;
|
|
||||||
using Электронная_Фармация.Properties;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using Электронная_Фармация.Classes;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Windows.Forms;
|
||||||
using Elfisa.UI.Helpers;
|
using Elfisa.UI.Helpers;
|
||||||
using Elfisa.UI.Theming;
|
using Elfisa.UI.Theming;
|
||||||
|
using Электронная_Фармация.Classes;
|
||||||
|
using Электронная_Фармация.Properties;
|
||||||
|
|
||||||
namespace Электронная_Фармация.HelpForms
|
namespace Электронная_Фармация.HelpForms
|
||||||
{
|
{
|
||||||
@ -25,98 +16,79 @@ 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)
|
||||||
{
|
{
|
||||||
this.Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
this.UseWaitCursor = true;
|
UseWaitCursor = true;
|
||||||
string strLogin = txtLogin.Text;
|
string strLogin = (txtLogin.Text ?? string.Empty).Trim();
|
||||||
string strPassword = txtPassword.Text;
|
string strPassword = txtPassword.Text ?? string.Empty;
|
||||||
|
|
||||||
if (strLogin.Length > 0 && strPassword.Length > 0)
|
if (strLogin.Length > 0 && strPassword.Length > 0)
|
||||||
{
|
{
|
||||||
// Адрес API берём из поля и сохраняем ДО входа: иначе логин уходит
|
string apiUrl = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
|
||||||
// на старый сохранённый/дефолтный адрес, а не на введённый пользователем.
|
? AppConfig.DefaultApiBaseUrl
|
||||||
string apiUrl = txtApiUrl.Text.Trim();
|
: Settings.Default.ApiBaseUrl;
|
||||||
Settings.Default.ApiBaseUrl = apiUrl;
|
|
||||||
Settings.Default.LocationId = txtLocationId.Text.Trim();
|
|
||||||
Settings.Default.Save();
|
|
||||||
|
|
||||||
LoginRequest loginRequest = new LoginRequest();
|
LoginRequest loginRequest = new LoginRequest();
|
||||||
loginRequest.Username = strLogin.Trim();
|
loginRequest.Username = strLogin;
|
||||||
loginRequest.Password = strPassword.Trim();
|
loginRequest.Password = strPassword.Trim();
|
||||||
|
|
||||||
var client = new ApiClient(apiUrl);
|
var client = new ApiClient(AppConfig.NormalizeApiBaseUrl(apiUrl));
|
||||||
|
AppDebugLog.Info("Auth", $"Попытка авторизации. Сервер={client.BaseUrl}, login={loginRequest.Username}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
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 = strPassword;
|
Settings.Default.stringPassword = string.Empty;
|
||||||
Settings.Default.stringToken = token;
|
Settings.Default.stringToken = token;
|
||||||
Settings.Default.LocationId = txtLocationId.Text.Trim();
|
Settings.Default.ApiBaseUrl = client.BaseUrl;
|
||||||
Settings.Default.ApiBaseUrl = txtApiUrl.Text.Trim();
|
|
||||||
Settings.Default.Save();
|
Settings.Default.Save();
|
||||||
|
|
||||||
|
AppDebugLog.Info("Auth", $"Авторизация успешна. token={AppDebugLog.MaskToken(token)}");
|
||||||
ToastNotification.ShowSuccess("Авторизация успешно выполнена");
|
ToastNotification.ShowSuccess("Авторизация успешно выполнена");
|
||||||
//MessageBox.Show("Данные были сохранены", "Регистрация программы успешно выполнена", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
||||||
|
|
||||||
// Токен автоматически сохраняется в клиенте
|
|
||||||
// и будет использоваться для всех последующих запросов
|
|
||||||
DialogResult = DialogResult.OK;
|
DialogResult = DialogResult.OK;
|
||||||
Close();
|
Close();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
catch (UnauthorizedAccessException ex)
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("Auth", "Ошибка авторизации", ex);
|
||||||
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
|
ToastNotification.ShowError($"Ошибка авторизации: {ex.Message}");
|
||||||
//MessageBox.Show($"Ошибка авторизации: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("Auth", "Ошибка сети при авторизации", ex);
|
||||||
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
|
ToastNotification.ShowError($"Ошибка сети: {ex.Message}");
|
||||||
//MessageBox.Show($"Ошибка сети: {ex.Message}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UiDialogs.ShowError("Есть незаполненные данные. Регистрация программы не пройдена", "Ошибка ввода", this);
|
UiDialogs.ShowError("Есть незаполненные данные. Регистрация программы не пройдена", "Ошибка ввода", this);
|
||||||
}
|
}
|
||||||
|
|
||||||
btnConfirmRegistration.Enabled = true;
|
btnConfirmRegistration.Enabled = true;
|
||||||
btnCloseThis.Enabled = true;
|
btnCloseThis.Enabled = true;
|
||||||
this.UseWaitCursor = false;
|
UseWaitCursor = false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HF_Registration_Load(object sender, EventArgs e)
|
private void HF_Registration_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
UiThemeHelper.ApplyToControlTree(this);
|
UiThemeHelper.ApplyToControlTree(this);
|
||||||
WindowChromeHelper.ApplyDialogChrome(this, panel1, label3);
|
WindowChromeHelper.ApplyDialogChrome(this, panel1, label3);
|
||||||
txtLogin.Text = login;
|
// Только последний успешный логин; пароль пользователь вводит каждый раз.
|
||||||
txtPassword.Text = password;
|
txtLogin.Text = Settings.Default.stringLogin ?? string.Empty;
|
||||||
txtLocationId.Text = Settings.Default.LocationId ?? string.Empty;
|
txtPassword.Text = string.Empty;
|
||||||
txtApiUrl.Text = string.IsNullOrWhiteSpace(Settings.Default.ApiBaseUrl)
|
checkTokenGained.Checked = !string.IsNullOrWhiteSpace(Settings.Default.stringToken);
|
||||||
? AppConfig.DefaultApiBaseUrl
|
|
||||||
: Settings.Default.ApiBaseUrl;
|
|
||||||
|
|
||||||
if (doWeHaveAToken.Length > 0)
|
|
||||||
{
|
|
||||||
checkTokenGained.Checked = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
checkTokenGained.Checked = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
192
src/ElectronicPharmacy/HelpForms/HF_Settings.Designer.cs
generated
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
95
src/ElectronicPharmacy/HelpForms/HF_Settings.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
src/ElectronicPharmacy/HelpForms/HF_UpdateAvailable.Designer.cs
generated
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/ElectronicPharmacy/HelpForms/HF_UpdateAvailable.cs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,9 +39,8 @@ namespace Электронная_Фармация.HelpForms
|
|||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch(Exception ex)
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("Upload", "Не удалось отправить данные на сервер", ex);
|
||||||
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
ToastNotification.ShowError($"Не удалось отправить данные. {ex.Message}");
|
||||||
//MessageBox.Show(ex.ToString(), "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
//MessageBox.Show($"Не удалось отправить данные на сервер. Текст ошибки:\n{ex.ToString()}", "Ошибка отправки данных на сервер", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using System.Net;
|
||||||
|
using Электронная_Фармация.Classes;
|
||||||
|
|
||||||
namespace Электронная_Фармация
|
namespace Электронная_Фармация
|
||||||
{
|
{
|
||||||
@ -8,6 +10,10 @@ namespace Электронная_Фармация
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
static void Main()
|
static void Main()
|
||||||
{
|
{
|
||||||
|
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
||||||
|
AppConfig.MigrateLegacySettings();
|
||||||
|
AppConfig.UpgradeUserSettingsIfNeeded();
|
||||||
|
AppDebugLog.Info("App", $"Запуск. ApiBaseUrl={AppConfig.ApiBaseUrl}, Log={AppDebugLog.CurrentLogFilePath}");
|
||||||
Application.EnableVisualStyles();
|
Application.EnableVisualStyles();
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
Application.SetCompatibleTextRenderingDefault(false);
|
||||||
Application.Run(new ElectroPharmacy());
|
Application.Run(new ElectroPharmacy());
|
||||||
|
|||||||
@ -2,35 +2,36 @@
|
|||||||
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.0.0")]
|
[assembly: AssemblyVersion("1.0.29.0")]
|
||||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
[assembly: AssemblyFileVersion("1.0.29.0")]
|
||||||
|
|
||||||
|
|||||||
112
src/ElectronicPharmacy/Properties/Settings.Designer.cs
generated
@ -1,4 +1,4 @@
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// <auto-generated>
|
// <auto-generated>
|
||||||
// Этот код создан программой.
|
// Этот код создан программой.
|
||||||
// Исполняемая версия:4.0.30319.42000
|
// Исполняемая версия:4.0.30319.42000
|
||||||
@ -34,6 +34,18 @@ namespace Электронная_Фармация.Properties {
|
|||||||
this["IntDefaultPercentMarkup"] = value;
|
this["IntDefaultPercentMarkup"] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Configuration.DefaultSettingValueAttribute("0")]
|
||||||
|
public decimal ClientMarkupPercent {
|
||||||
|
get {
|
||||||
|
return ((decimal)(this["ClientMarkupPercent"]));
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
this["ClientMarkupPercent"] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
@ -85,7 +97,7 @@ namespace Электронная_Фармация.Properties {
|
|||||||
|
|
||||||
[global::System.Configuration.UserScopedSettingAttribute()]
|
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Configuration.DefaultSettingValueAttribute("http://195.34.241.84:9988")]
|
[global::System.Configuration.DefaultSettingValueAttribute("https://24pharmdata.ru")]
|
||||||
public string ApiBaseUrl {
|
public string ApiBaseUrl {
|
||||||
get {
|
get {
|
||||||
return ((string)(this["ApiBaseUrl"]));
|
return ((string)(this["ApiBaseUrl"]));
|
||||||
@ -119,5 +131,101 @@ 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.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Configuration.DefaultSettingValueAttribute("")]
|
||||||
|
public string InvoiceExportPath {
|
||||||
|
get {
|
||||||
|
return ((string)(this["InvoiceExportPath"]));
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
this["InvoiceExportPath"] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
<?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>
|
||||||
<Setting Name="IntDefaultPercentMarkup" Type="System.Int32" Scope="User">
|
<Setting Name="IntDefaultPercentMarkup" Type="System.Int32" Scope="User">
|
||||||
<Value Profile="(Default)">0</Value>
|
<Value Profile="(Default)">0</Value>
|
||||||
</Setting>
|
</Setting>
|
||||||
|
<Setting Name="ClientMarkupPercent" Type="System.Decimal" Scope="User">
|
||||||
|
<Value Profile="(Default)">0</Value>
|
||||||
|
</Setting>
|
||||||
<Setting Name="stringLogin" Type="System.String" Scope="User">
|
<Setting Name="stringLogin" Type="System.String" Scope="User">
|
||||||
<Value Profile="(Default)" />
|
<Value Profile="(Default)" />
|
||||||
</Setting>
|
</Setting>
|
||||||
@ -18,7 +21,7 @@
|
|||||||
<Value Profile="(Default)" />
|
<Value Profile="(Default)" />
|
||||||
</Setting>
|
</Setting>
|
||||||
<Setting Name="ApiBaseUrl" Type="System.String" Scope="User">
|
<Setting Name="ApiBaseUrl" Type="System.String" Scope="User">
|
||||||
<Value Profile="(Default)">http://195.34.241.84:9988</Value>
|
<Value Profile="(Default)">https://24pharmdata.ru</Value>
|
||||||
</Setting>
|
</Setting>
|
||||||
<Setting Name="RegionId" Type="System.String" Scope="User">
|
<Setting Name="RegionId" Type="System.String" Scope="User">
|
||||||
<Value Profile="(Default)" />
|
<Value Profile="(Default)" />
|
||||||
@ -26,5 +29,29 @@
|
|||||||
<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">
|
||||||
|
<Value Profile="(Default)" />
|
||||||
|
</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>
|
||||||
BIN
src/ElectronicPharmacy/Resources/elfisa.ico
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
@ -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("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), string.Empty);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), string.Empty);
|
||||||
|
|
||||||
loadSuppliers();
|
loadSuppliers();
|
||||||
comboSuppliers.Visible = true;
|
comboSuppliers.Visible = true;
|
||||||
@ -46,13 +46,54 @@ namespace Электронная_Фармация.UserControls
|
|||||||
ApplyToolbarLayout();
|
ApplyToolbarLayout();
|
||||||
ModernButtonStyles.ApplyClear(btnClearGoodSearch);
|
ModernButtonStyles.ApplyClear(btnClearGoodSearch);
|
||||||
ModernButtonStyles.ApplyClear(btnClearOrderNumberFilter);
|
ModernButtonStyles.ApplyClear(btnClearOrderNumberFilter);
|
||||||
|
InitializeOrderContextMenu();
|
||||||
label3.BringToFront();
|
label3.BringToFront();
|
||||||
panelToolbar.Resize += (_, __) => ApplyToolbarLayout();
|
panelToolbar.Resize += (_, __) => ApplyToolbarLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeOrderContextMenu()
|
||||||
|
{
|
||||||
|
if (_ordersMenu != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getInvoiceMenuItem = new ToolStripMenuItem("Получить накладную");
|
||||||
|
_getInvoiceMenuItem.Click += (_, __) => RequestInvoiceForSelectedOrder();
|
||||||
|
|
||||||
|
_ordersMenu = new ContextMenuStrip();
|
||||||
|
_ordersMenu.Items.Add(_getInvoiceMenuItem);
|
||||||
|
_ordersMenu.Opening += OrdersMenu_Opening;
|
||||||
|
|
||||||
|
DGVOrders.ContextMenuStrip = _ordersMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OrdersMenu_Opening(object sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
var pt = DGVOrders.PointToClient(Cursor.Position);
|
||||||
|
var hit = DGVOrders.HitTest(pt.X, pt.Y);
|
||||||
|
if (hit.RowIndex < 0 || hit.RowIndex >= DGVOrders.Rows.Count)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DGVOrders.ClearSelection();
|
||||||
|
DGVOrders.Rows[hit.RowIndex].Selected = true;
|
||||||
|
if (hit.ColumnIndex >= 0)
|
||||||
|
{
|
||||||
|
DGVOrders.CurrentCell = DGVOrders.Rows[hit.RowIndex].Cells[hit.ColumnIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
var status = DGVOrders.Rows[hit.RowIndex].Cells["Статус"]?.Value?.ToString();
|
||||||
|
_getInvoiceMenuItem.Enabled = string.Equals(status, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private ModernComboBox _comboOrderStatus;
|
private ModernComboBox _comboOrderStatus;
|
||||||
private ModernComboBox _comboConsignees;
|
private ModernComboBox _comboConsignees;
|
||||||
private ModernButton _btnSendOrder;
|
private ModernButton _btnSendOrder;
|
||||||
|
private ContextMenuStrip _ordersMenu;
|
||||||
|
private ToolStripMenuItem _getInvoiceMenuItem;
|
||||||
|
|
||||||
private void InitOrderToolbar()
|
private void InitOrderToolbar()
|
||||||
{
|
{
|
||||||
@ -293,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("dd.MM.yyyy"),
|
dtpDateFrom.Value.ToString("yyyy-MM-dd"),
|
||||||
dtpDateTo.Value.ToString("dd.MM.yyyy"),
|
dtpDateTo.Value.ToString("yyyy-MM-dd"),
|
||||||
addon);
|
addon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,15 +355,70 @@ namespace Электронная_Фармация.UserControls
|
|||||||
}
|
}
|
||||||
|
|
||||||
var orderId = DGVOrders.SelectedRows[0].Cells["ИД заказа"].Value.ToString();
|
var orderId = DGVOrders.SelectedRows[0].Cells["ИД заказа"].Value.ToString();
|
||||||
var sender = new DataSender();
|
try
|
||||||
await sender.SendSingleOrderAsync(orderId);
|
{
|
||||||
|
var sender = new DataSender();
|
||||||
|
await sender.SendSingleOrderAsync(orderId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppDebugLog.Error("Orders", $"Не удалось отправить заказ {orderId}", ex);
|
||||||
|
ToastNotification.ShowError($"Не удалось отправить заказ: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
ApplyOrderFilters();
|
ApplyOrderFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RequestInvoiceForSelectedOrder()
|
||||||
|
{
|
||||||
|
if (DGVOrders.SelectedRows.Count == 0)
|
||||||
|
{
|
||||||
|
ToastNotification.ShowCustom("Выберите отправленный заказ.", Color.DarkOrange, Color.White);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedRow = DGVOrders.SelectedRows[0];
|
||||||
|
var status = selectedRow.Cells["Статус"]?.Value?.ToString();
|
||||||
|
if (!string.Equals(status, "ОТПРАВЛЕН", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
ToastNotification.ShowCustom("Накладную можно получить только для отправленного заказа.", Color.DarkOrange, Color.White);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderId = selectedRow.Cells["ИД заказа"]?.Value?.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(orderId))
|
||||||
|
{
|
||||||
|
ToastNotification.ShowError("Не удалось определить идентификатор заказа.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var service = new InvoiceSyncService();
|
||||||
|
service.SyncFromServerAsync().GetAwaiter().GetResult();
|
||||||
|
ToastNotification.ShowSuccess("Накладные обновлены с сервера.");
|
||||||
|
|
||||||
|
var shell = FindForm() as ElectroPharmacy;
|
||||||
|
shell?.OpenInvoicesTab(orderId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ToastNotification.ShowError($"Не удалось получить накладную: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LoadOrders(string dateFrom, string dateTo, string filters)
|
void LoadOrders(string dateFrom, string dateTo, string filters)
|
||||||
{
|
{
|
||||||
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],
|
||||||
@ -337,8 +433,8 @@ select
|
|||||||
[ConsigneeName]
|
[ConsigneeName]
|
||||||
from Orders
|
from Orders
|
||||||
where
|
where
|
||||||
OrderDate >= '{dateFrom}'
|
({orderDateIso}) >= '{dateFrom}'
|
||||||
and OrderDate <= '{dateTo}'
|
and ({orderDateIso}) <= '{dateTo}'
|
||||||
{filters}
|
{filters}
|
||||||
";
|
";
|
||||||
|
|
||||||
@ -420,7 +516,11 @@ and OrderDate <= '{dateTo}'
|
|||||||
void loadConsignees()
|
void loadConsignees()
|
||||||
{
|
{
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
string qShowMeConsigneesList = "select ConsigneesName from Consignees order by [ConsigneesUseAsDefault] desc";
|
string qShowMeConsigneesList = @"
|
||||||
|
select distinct ConsigneeName
|
||||||
|
from Orders
|
||||||
|
where trim(ifnull(ConsigneeName, '')) <> ''
|
||||||
|
order by ConsigneeName";
|
||||||
|
|
||||||
using (SQLiteConnection conConsignees = new SQLiteConnection(connectionStringToLocalDB))
|
using (SQLiteConnection conConsignees = new SQLiteConnection(connectionStringToLocalDB))
|
||||||
{
|
{
|
||||||
@ -439,7 +539,7 @@ and OrderDate <= '{dateTo}'
|
|||||||
|
|
||||||
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["ConsigneesName"]?.ToString())
|
.Select(r => r["ConsigneeName"]?.ToString())
|
||||||
.Where(n => !string.IsNullOrWhiteSpace(n)));
|
.Where(n => !string.IsNullOrWhiteSpace(n)));
|
||||||
_comboConsignees.DataSource = items;
|
_comboConsignees.DataSource = items;
|
||||||
_comboConsignees.SelectedIndex = 0;
|
_comboConsignees.SelectedIndex = 0;
|
||||||
@ -459,7 +559,11 @@ and OrderDate <= '{dateTo}'
|
|||||||
{
|
{
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
|
|
||||||
string qShowMeSuppliersList = "select SuppliersName from suppliers order by [SuppliersName] asc";
|
string qShowMeSuppliersList = @"
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
@ -471,7 +575,7 @@ and OrderDate <= '{dateTo}'
|
|||||||
DataTable tableSuppliers = new DataTable();
|
DataTable tableSuppliers = new DataTable();
|
||||||
daSuppliersList.Fill(tableSuppliers);
|
daSuppliersList.Fill(tableSuppliers);
|
||||||
comboSuppliers.DataSource = tableSuppliers;
|
comboSuppliers.DataSource = tableSuppliers;
|
||||||
comboSuppliers.DisplayMember = "SuppliersName";
|
comboSuppliers.DisplayMember = "SupplierName";
|
||||||
comboSuppliers.Text = "Поставщик";
|
comboSuppliers.Text = "Поставщик";
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -588,25 +692,25 @@ and OrderDate <= '{dateTo}'
|
|||||||
{
|
{
|
||||||
txtGoodFromOrders.Text = "Товар";
|
txtGoodFromOrders.Text = "Товар";
|
||||||
filter = string.Empty;
|
filter = string.Empty;
|
||||||
LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dtpDateFrom_ValueChanged(object sender, EventArgs e)
|
private void dtpDateFrom_ValueChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
|
|
||||||
LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void dtpDateTo_ValueChanged(object sender, EventArgs e)
|
private void dtpDateTo_ValueChanged(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), 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("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,7 +718,7 @@ and OrderDate <= '{dateTo}'
|
|||||||
{
|
{
|
||||||
if (e.KeyChar == (Char)Keys.Enter)
|
if (e.KeyChar == (Char)Keys.Enter)
|
||||||
{
|
{
|
||||||
LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,7 +742,7 @@ and OrderDate <= '{dateTo}'
|
|||||||
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("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -785,7 +889,7 @@ where [id_order] = '{idPriceList}'
|
|||||||
and OrderNumber like '%{txtOrderNumberFilter.Text}%'
|
and OrderNumber like '%{txtOrderNumberFilter.Text}%'
|
||||||
";
|
";
|
||||||
|
|
||||||
LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -810,7 +914,7 @@ and OrderNumber like '%{txtOrderNumberFilter.Text}%'
|
|||||||
{
|
{
|
||||||
txtOrderNumberFilter.Text = "Номер заказа";
|
txtOrderNumberFilter.Text = "Номер заказа";
|
||||||
filter = string.Empty;
|
filter = string.Empty;
|
||||||
LoadOrders(dtpDateFrom.Value.ToString("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), filter);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void btnAddComment_Click(object sender, EventArgs e)
|
private void btnAddComment_Click(object sender, EventArgs e)
|
||||||
@ -823,7 +927,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("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), string.Empty);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -860,7 +964,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("dd.MM.yyyy"), dtpDateTo.Value.ToString("dd.MM.yyyy"), string.Empty);
|
LoadOrders(dtpDateFrom.Value.ToString("yyyy-MM-dd"), dtpDateTo.Value.ToString("yyyy-MM-dd"), string.Empty);
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch(Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -237,8 +237,7 @@
|
|||||||
//
|
//
|
||||||
// UCInvoices
|
// UCInvoices
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F);
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
|
|
||||||
this.Controls.Add(this.btnClearConsignees);
|
this.Controls.Add(this.btnClearConsignees);
|
||||||
this.Controls.Add(this.btnClearSuppliers);
|
this.Controls.Add(this.btnClearSuppliers);
|
||||||
this.Controls.Add(this.PContentInvoice);
|
this.Controls.Add(this.PContentInvoice);
|
||||||
|
|||||||
@ -15,9 +15,30 @@ namespace Электронная_Фармация.UserControls
|
|||||||
{
|
{
|
||||||
public partial class UCInvoices : UserControl
|
public partial class UCInvoices : UserControl
|
||||||
{
|
{
|
||||||
|
private string _initialSourceOrderId;
|
||||||
|
private ContextMenuStrip _invoiceMenu;
|
||||||
|
private ToolStripMenuItem _exportInvoiceMenuItem;
|
||||||
|
private Panel panelToolbar;
|
||||||
|
private bool _toolbarReady;
|
||||||
|
|
||||||
public UCInvoices()
|
public UCInvoices()
|
||||||
|
: this(null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UCInvoices(string sourceOrderId)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
_initialSourceOrderId = sourceOrderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSourceOrderFilter(string sourceOrderId)
|
||||||
|
{
|
||||||
|
_initialSourceOrderId = sourceOrderId;
|
||||||
|
if (IsHandleCreated)
|
||||||
|
{
|
||||||
|
getDatesFromCalendar();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void getDatesFromCalendar()
|
void getDatesFromCalendar()
|
||||||
@ -36,7 +57,11 @@ namespace Электронная_Фармация.UserControls
|
|||||||
{
|
{
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
|
|
||||||
string qShowMeSuppliersList = "select SuppliersName from suppliers order by [SuppliersName] asc";
|
string qShowMeSuppliersList = @"
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
@ -48,7 +73,7 @@ namespace Электронная_Фармация.UserControls
|
|||||||
DataTable tableSuppliers = new DataTable();
|
DataTable tableSuppliers = new DataTable();
|
||||||
daSuppliersList.Fill(tableSuppliers);
|
daSuppliersList.Fill(tableSuppliers);
|
||||||
comboSuppliers.DataSource = tableSuppliers;
|
comboSuppliers.DataSource = tableSuppliers;
|
||||||
comboSuppliers.DisplayMember = "SuppliersName";
|
comboSuppliers.DisplayMember = "SupplierName";
|
||||||
comboSuppliers.Text = "Поставщик";
|
comboSuppliers.Text = "Поставщик";
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch(Exception ex)
|
||||||
@ -66,7 +91,11 @@ namespace Электронная_Фармация.UserControls
|
|||||||
{
|
{
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
|
|
||||||
string qShowMeConsigneesList = "select ConsigneesName from Consignees order by [ConsigneesUseAsDefault] desc";
|
string qShowMeConsigneesList = @"
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
@ -94,52 +123,80 @@ namespace Электронная_Фармация.UserControls
|
|||||||
|
|
||||||
void loadInvoicesList(string InvoicesDateFrom, string InvoicesDateTo, string dateFromYearFormat)
|
void loadInvoicesList(string InvoicesDateFrom, string InvoicesDateTo, string dateFromYearFormat)
|
||||||
{
|
{
|
||||||
|
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
|
|
||||||
string filterAddons = $"where i.InvoiceDate between date('{InvoicesDateFrom}') and date('{InvoicesDateTo}')";
|
|
||||||
|
|
||||||
if (txtInvoiceNumber.Text != "" & txtInvoiceNumber.Text != "Номер накладной")
|
|
||||||
{
|
|
||||||
filterAddons += $" and i.InvoiceNumber like '%{txtInvoiceNumber.Text}%'";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comboSuppliers.Text != "" & comboSuppliers.Text != "Поставщик")
|
|
||||||
{
|
|
||||||
filterAddons += $" and s.SuppliersName like '%{comboSuppliers.Text}%'";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comboConsignees.Text != "" & comboConsignees.Text != "Грузополучатель")
|
|
||||||
{
|
|
||||||
filterAddons += $" and c.ConsigneesName like '%{comboConsignees.Text}%'";
|
|
||||||
}
|
|
||||||
|
|
||||||
string qShowMeListOfInvoices = $@"
|
|
||||||
select i.InvoiceNumber as [Номер],
|
|
||||||
i.invoiceDate as [Дата],
|
|
||||||
s.SuppliersName as [Поставщик],
|
|
||||||
c.ConsigneesName as [Грузополучатель],
|
|
||||||
c.ConsigneesAddress as [Адрес ГП],
|
|
||||||
i.SumWithoutNDS as [Сумма без НДС],
|
|
||||||
i.SumWithNDS as [Сумма с НДС],
|
|
||||||
i.SumNDS as [Сумма НДС]
|
|
||||||
from [Invoice] i
|
|
||||||
inner join [Suppliers] s on s.codeSuppliers = i.CodeSupplier
|
|
||||||
inner join [Consignees] c on c.codeConsignees = i.CodeConsignees
|
|
||||||
{filterAddons}
|
|
||||||
order by i.InvoiceDate asc
|
|
||||||
";
|
|
||||||
|
|
||||||
using (SQLiteConnection conInvoices = new SQLiteConnection(connectionStringToLocalDB))
|
using (SQLiteConnection conInvoices = new SQLiteConnection(connectionStringToLocalDB))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
conInvoices.Open();
|
conInvoices.Open();
|
||||||
SQLiteCommand cmdShowInvoicesList = new SQLiteCommand(qShowMeListOfInvoices, conInvoices);
|
var sql = @"
|
||||||
|
select
|
||||||
|
i.[idInvoice] as [ИД накладной],
|
||||||
|
i.[InvoiceNumber] as [Номер],
|
||||||
|
i.[InvoiceDate] as [Дата],
|
||||||
|
ifnull(i.[SupplierName], '') as [Поставщик],
|
||||||
|
ifnull(i.[ConsigneesName], '') as [Грузополучатель],
|
||||||
|
i.[SourceOrderNumber] as [Номер заказа],
|
||||||
|
coalesce(i.[InvoiceSum], i.[SumWithNDS], i.[SumWithoutNDS], 0) as [Сумма],
|
||||||
|
coalesce(i.[RefuseSum], 0) as [Сумма отказа]
|
||||||
|
from [Invoice] i
|
||||||
|
where date(i.[InvoiceDate]) between date(@dateFrom) and date(@dateTo)";
|
||||||
|
|
||||||
|
if (txtInvoiceNumber.Text != "" && txtInvoiceNumber.Text != "Номер накладной")
|
||||||
|
{
|
||||||
|
sql += " and i.[InvoiceNumber] like @invoiceNumber";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик")
|
||||||
|
{
|
||||||
|
sql += " and ifnull(i.[SupplierName], '') like @supplierName";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель")
|
||||||
|
{
|
||||||
|
sql += " and ifnull(i.[ConsigneesName], '') like @consigneeName";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_initialSourceOrderId))
|
||||||
|
{
|
||||||
|
sql += " and cast(i.[SourceOrderId] as text) = @sourceOrderId";
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += " order by date(i.[InvoiceDate]) desc, i.[idInvoice] desc;";
|
||||||
|
|
||||||
|
SQLiteCommand cmdShowInvoicesList = new SQLiteCommand(sql, conInvoices);
|
||||||
|
cmdShowInvoicesList.Parameters.AddWithValue("@dateFrom", InvoicesDateFrom);
|
||||||
|
cmdShowInvoicesList.Parameters.AddWithValue("@dateTo", InvoicesDateTo);
|
||||||
|
if (txtInvoiceNumber.Text != "" && txtInvoiceNumber.Text != "Номер накладной")
|
||||||
|
{
|
||||||
|
cmdShowInvoicesList.Parameters.AddWithValue("@invoiceNumber", $"%{txtInvoiceNumber.Text}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comboSuppliers.Text != "" && comboSuppliers.Text != "Поставщик")
|
||||||
|
{
|
||||||
|
cmdShowInvoicesList.Parameters.AddWithValue("@supplierName", $"%{comboSuppliers.Text}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comboConsignees.Text != "" && comboConsignees.Text != "Грузополучатель")
|
||||||
|
{
|
||||||
|
cmdShowInvoicesList.Parameters.AddWithValue("@consigneeName", $"%{comboConsignees.Text}%");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_initialSourceOrderId))
|
||||||
|
{
|
||||||
|
cmdShowInvoicesList.Parameters.AddWithValue("@sourceOrderId", _initialSourceOrderId);
|
||||||
|
}
|
||||||
|
|
||||||
DataTable tableInvoices = new DataTable();
|
DataTable tableInvoices = new DataTable();
|
||||||
SQLiteDataAdapter daInvoicesList = new SQLiteDataAdapter(cmdShowInvoicesList);
|
SQLiteDataAdapter daInvoicesList = new SQLiteDataAdapter(cmdShowInvoicesList);
|
||||||
daInvoicesList.Fill(tableInvoices);
|
daInvoicesList.Fill(tableInvoices);
|
||||||
dgvInvoice.DataSource = tableInvoices;
|
dgvInvoice.DataSource = tableInvoices;
|
||||||
|
|
||||||
|
if (dgvInvoice.Columns.Count > 0)
|
||||||
|
{
|
||||||
|
dgvInvoice.Columns["ИД накладной"].Visible = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -162,18 +219,171 @@ order by i.InvoiceDate asc
|
|||||||
{
|
{
|
||||||
UiThemeHelper.ApplyToControlTree(this);
|
UiThemeHelper.ApplyToControlTree(this);
|
||||||
Font = new Font("Segoe UI", 9.75f, FontStyle.Regular);
|
Font = new Font("Segoe UI", 9.75f, FontStyle.Regular);
|
||||||
|
InitializeInvoiceContextMenu();
|
||||||
|
EnsureInvoiceToolbar();
|
||||||
|
|
||||||
button1.Visible = true;
|
button1.Visible = true;
|
||||||
button1.Text = "Обновить с сервера";
|
button1.Text = "Обновить с сервера";
|
||||||
button1.Width = 180;
|
|
||||||
ModernButtonStyles.ApplyAuto(button1);
|
ModernButtonStyles.ApplyAuto(button1);
|
||||||
button1.Click += async (_, __) => await SyncFromServerAsync();
|
button1.Click -= Button1_SyncClick;
|
||||||
|
button1.Click += Button1_SyncClick;
|
||||||
|
|
||||||
|
ModernButtonStyles.ApplyClear(btnClearSuppliers);
|
||||||
|
ModernButtonStyles.ApplyClear(btnClearConsignees);
|
||||||
|
btnClearSuppliers.Text = "×";
|
||||||
|
btnClearConsignees.Text = "×";
|
||||||
|
|
||||||
|
ApplyToolbarLayout();
|
||||||
|
|
||||||
loadConsignees();
|
loadConsignees();
|
||||||
loadSuppliers();
|
loadSuppliers();
|
||||||
getDatesFromCalendar();
|
getDatesFromCalendar();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void Button1_SyncClick(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
await SyncFromServerAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureInvoiceToolbar()
|
||||||
|
{
|
||||||
|
if (_toolbarReady)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
panelToolbar = new Panel
|
||||||
|
{
|
||||||
|
Name = "panelToolbar",
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Height = 96,
|
||||||
|
Padding = new Padding(12, 10, 12, 14),
|
||||||
|
Tag = "toolbar"
|
||||||
|
};
|
||||||
|
|
||||||
|
var toolbarControls = new Control[]
|
||||||
|
{
|
||||||
|
button1,
|
||||||
|
dtpDateFrom,
|
||||||
|
label1,
|
||||||
|
dtpDateTo,
|
||||||
|
txtInvoiceNumber,
|
||||||
|
comboSuppliers,
|
||||||
|
btnClearSuppliers,
|
||||||
|
comboConsignees,
|
||||||
|
btnClearConsignees
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var control in toolbarControls)
|
||||||
|
{
|
||||||
|
Controls.Remove(control);
|
||||||
|
panelToolbar.Controls.Add(control);
|
||||||
|
}
|
||||||
|
|
||||||
|
Controls.Add(panelToolbar);
|
||||||
|
panelToolbar.BringToFront();
|
||||||
|
|
||||||
|
PContentInvoice.Dock = DockStyle.Fill;
|
||||||
|
PContentInvoice.BringToFront();
|
||||||
|
panelToolbar.BringToFront();
|
||||||
|
|
||||||
|
panelToolbar.Resize += (_, __) => ApplyToolbarLayout();
|
||||||
|
Resize += (_, __) => ApplyToolbarLayout();
|
||||||
|
_toolbarReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyToolbarLayout()
|
||||||
|
{
|
||||||
|
if (panelToolbar == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int pad = 12;
|
||||||
|
const int gap = 8;
|
||||||
|
const int clearGap = 4;
|
||||||
|
const int controlHeight = 32;
|
||||||
|
const int buttonHeight = 34;
|
||||||
|
const int rowGap = 10;
|
||||||
|
const int refreshWidth = 180;
|
||||||
|
const int dateWidth = 112;
|
||||||
|
const int clearSize = 32;
|
||||||
|
const int invoicePreferred = 200;
|
||||||
|
const int invoiceMin = 140;
|
||||||
|
const int supplierPreferred = 170;
|
||||||
|
const int consigneePreferred = 190;
|
||||||
|
const int supplierMin = 130;
|
||||||
|
const int consigneeMin = 150;
|
||||||
|
|
||||||
|
var panelWidth = Math.Max(panelToolbar.ClientSize.Width, 360);
|
||||||
|
int y = 10;
|
||||||
|
int x = pad;
|
||||||
|
|
||||||
|
button1.SetBounds(x, y, refreshWidth, buttonHeight);
|
||||||
|
x = button1.Right + gap;
|
||||||
|
|
||||||
|
dtpDateFrom.SetBounds(x, y + 1, dateWidth, controlHeight);
|
||||||
|
x = dtpDateFrom.Right + 6;
|
||||||
|
label1.AutoSize = true;
|
||||||
|
label1.Location = new Point(x, y + 5);
|
||||||
|
x = label1.Right + 6;
|
||||||
|
dtpDateTo.SetBounds(x, y + 1, dateWidth, controlHeight);
|
||||||
|
x = dtpDateTo.Right + gap;
|
||||||
|
|
||||||
|
int invoiceWidth = Math.Max(invoiceMin, Math.Min(invoicePreferred, panelWidth - pad - x));
|
||||||
|
if (invoiceWidth < invoiceMin || x + invoiceMin > panelWidth - pad)
|
||||||
|
{
|
||||||
|
y += buttonHeight + rowGap;
|
||||||
|
x = pad;
|
||||||
|
invoiceWidth = Math.Max(invoiceMin, Math.Min(invoicePreferred, panelWidth - pad * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
txtInvoiceNumber.SetBounds(x, y + 1, invoiceWidth, controlHeight);
|
||||||
|
|
||||||
|
y += buttonHeight + rowGap;
|
||||||
|
x = pad;
|
||||||
|
|
||||||
|
int filtersAvailable = panelWidth - pad * 2 - clearSize * 2 - clearGap * 2 - gap;
|
||||||
|
int supplierWidth;
|
||||||
|
int consigneeWidth;
|
||||||
|
if (filtersAvailable >= supplierPreferred + consigneePreferred)
|
||||||
|
{
|
||||||
|
supplierWidth = supplierPreferred;
|
||||||
|
consigneeWidth = consigneePreferred;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float scale = filtersAvailable / (float)(supplierPreferred + consigneePreferred);
|
||||||
|
supplierWidth = Math.Max(supplierMin, (int)(supplierPreferred * scale));
|
||||||
|
consigneeWidth = Math.Max(consigneeMin, filtersAvailable - supplierWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pad + supplierWidth + clearGap + clearSize + gap + consigneeWidth + clearGap + clearSize > panelWidth)
|
||||||
|
{
|
||||||
|
// Wrap consignee to next row on very narrow widths.
|
||||||
|
supplierWidth = Math.Max(supplierMin, panelWidth - pad * 2 - clearGap - clearSize);
|
||||||
|
comboSuppliers.SetBounds(x, y + 1, supplierWidth, controlHeight);
|
||||||
|
btnClearSuppliers.SetBounds(comboSuppliers.Right + clearGap, y, clearSize, controlHeight);
|
||||||
|
|
||||||
|
y += buttonHeight + rowGap;
|
||||||
|
x = pad;
|
||||||
|
consigneeWidth = Math.Max(consigneeMin, panelWidth - pad * 2 - clearGap - clearSize);
|
||||||
|
comboConsignees.SetBounds(x, y + 1, consigneeWidth, controlHeight);
|
||||||
|
btnClearConsignees.SetBounds(comboConsignees.Right + clearGap, y, clearSize, controlHeight);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
comboSuppliers.SetBounds(x, y + 1, supplierWidth, controlHeight);
|
||||||
|
btnClearSuppliers.SetBounds(comboSuppliers.Right + clearGap, y, clearSize, controlHeight);
|
||||||
|
x = btnClearSuppliers.Right + gap;
|
||||||
|
comboConsignees.SetBounds(x, y + 1, consigneeWidth, controlHeight);
|
||||||
|
btnClearConsignees.SetBounds(comboConsignees.Right + clearGap, y, clearSize, controlHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
panelToolbar.Height = y + buttonHeight + pad + 4;
|
||||||
|
panelToolbar.Padding = new Padding(pad, 10, pad, 14);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SyncFromServerAsync()
|
private async Task SyncFromServerAsync()
|
||||||
{
|
{
|
||||||
button1.Enabled = false;
|
button1.Enabled = false;
|
||||||
@ -187,6 +397,7 @@ order by i.InvoiceDate asc
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("Invoices", "Не удалось загрузить накладные с API", ex);
|
||||||
ToastNotification.ShowError($"Не удалось загрузить накладные: {ex.Message}");
|
ToastNotification.ShowError($"Не удалось загрузить накладные: {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -196,6 +407,43 @@ order by i.InvoiceDate asc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void InitializeInvoiceContextMenu()
|
||||||
|
{
|
||||||
|
if (_invoiceMenu != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_exportInvoiceMenuItem = new ToolStripMenuItem("Экспортировать в папку");
|
||||||
|
_exportInvoiceMenuItem.Click += (_, __) => ExportSelectedInvoice();
|
||||||
|
|
||||||
|
_invoiceMenu = new ContextMenuStrip();
|
||||||
|
_invoiceMenu.Items.Add(_exportInvoiceMenuItem);
|
||||||
|
_invoiceMenu.Opening += InvoiceMenu_Opening;
|
||||||
|
|
||||||
|
dgvInvoice.ContextMenuStrip = _invoiceMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvoiceMenu_Opening(object sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
var pt = dgvInvoice.PointToClient(Cursor.Position);
|
||||||
|
var hit = dgvInvoice.HitTest(pt.X, pt.Y);
|
||||||
|
if (hit.RowIndex < 0 || hit.RowIndex >= dgvInvoice.Rows.Count)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dgvInvoice.ClearSelection();
|
||||||
|
dgvInvoice.Rows[hit.RowIndex].Selected = true;
|
||||||
|
if (hit.ColumnIndex >= 0)
|
||||||
|
{
|
||||||
|
dgvInvoice.CurrentCell = dgvInvoice.Rows[hit.RowIndex].Cells[hit.ColumnIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
_exportInvoiceMenuItem.Enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private void dtpDateTo_Leave(object sender, EventArgs e)
|
private void dtpDateTo_Leave(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
@ -226,28 +474,22 @@ order by i.InvoiceDate asc
|
|||||||
|
|
||||||
void selectInvoice()
|
void selectInvoice()
|
||||||
{
|
{
|
||||||
if (dgvInvoice.Rows.Count > 0)
|
if (dgvInvoice.Rows.Count > 0 && dgvInvoice.CurrentCell != null)
|
||||||
{
|
{
|
||||||
int SelectedInvoiceIndex = dgvInvoice.CurrentCell.RowIndex;
|
int SelectedInvoiceIndex = dgvInvoice.CurrentCell.RowIndex;
|
||||||
string SelectedInvoiceNumber = dgvInvoice.Rows[SelectedInvoiceIndex].Cells[0].Value.ToString();
|
string SelectedInvoiceId = dgvInvoice.Rows[SelectedInvoiceIndex].Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty;
|
||||||
loadInvoiceItems(SelectedInvoiceNumber);
|
loadInvoiceItems(SelectedInvoiceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadInvoiceItems(string InvoiceNumber)
|
void loadInvoiceItems(string invoiceId)
|
||||||
{
|
{
|
||||||
string filterAddons = string.Empty;
|
|
||||||
|
|
||||||
|
|
||||||
string InvNum = InvoiceNumber;
|
|
||||||
if (dgvInvoice.Rows.Count > 0)
|
if (dgvInvoice.Rows.Count > 0)
|
||||||
{
|
{
|
||||||
if (InvNum != "" | InvoiceNumber != string.Empty)
|
if (!string.IsNullOrWhiteSpace(invoiceId))
|
||||||
{
|
{
|
||||||
filterAddons = $"where i.InvoiceNumber = '{InvNum}'";
|
|
||||||
|
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
string qShowMeInvoiceItems = $@"
|
string qShowMeInvoiceItems = @"
|
||||||
select
|
select
|
||||||
ii.GoodCode as [Код товара],
|
ii.GoodCode as [Код товара],
|
||||||
ii.Good as [Товар],
|
ii.Good as [Товар],
|
||||||
@ -270,20 +512,22 @@ ii.Sertificate as [Сертификат],
|
|||||||
ii.Marked as [Признак маркировки],
|
ii.Marked as [Признак маркировки],
|
||||||
ii.GTIN as [GTIN]
|
ii.GTIN as [GTIN]
|
||||||
from InvoiceItem ii
|
from InvoiceItem ii
|
||||||
inner join Invoice i on i.idInvoice = ii.idInvoice
|
where ii.idInvoice = @invoiceId;";
|
||||||
{filterAddons}
|
|
||||||
";
|
|
||||||
using (SQLiteConnection conInvoiceItems = new SQLiteConnection(connectionStringToLocalDB))
|
using (SQLiteConnection conInvoiceItems = new SQLiteConnection(connectionStringToLocalDB))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
conInvoiceItems.Open();
|
conInvoiceItems.Open();
|
||||||
SQLiteCommand cmdShowMeInvoiceItems = new SQLiteCommand(qShowMeInvoiceItems, conInvoiceItems);
|
SQLiteCommand cmdShowMeInvoiceItems = new SQLiteCommand(qShowMeInvoiceItems, conInvoiceItems);
|
||||||
|
cmdShowMeInvoiceItems.Parameters.AddWithValue("@invoiceId", invoiceId);
|
||||||
DataTable tableInvoiceItems = new DataTable();
|
DataTable tableInvoiceItems = new DataTable();
|
||||||
SQLiteDataAdapter daInvoiceItems = new SQLiteDataAdapter(cmdShowMeInvoiceItems);
|
SQLiteDataAdapter daInvoiceItems = new SQLiteDataAdapter(cmdShowMeInvoiceItems);
|
||||||
daInvoiceItems.Fill(tableInvoiceItems);
|
daInvoiceItems.Fill(tableInvoiceItems);
|
||||||
dgvInvoiceItems.DataSource = tableInvoiceItems;
|
dgvInvoiceItems.DataSource = tableInvoiceItems;
|
||||||
dgvInvoiceItems.Columns[10].ValueType = typeof(bool);
|
if (dgvInvoiceItems.Columns.Count > 10)
|
||||||
|
{
|
||||||
|
dgvInvoiceItems.Columns[10].ValueType = typeof(bool);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -303,6 +547,37 @@ inner join Invoice i on i.idInvoice = ii.idInvoice
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ExportSelectedInvoice()
|
||||||
|
{
|
||||||
|
if (dgvInvoice.SelectedRows.Count == 0)
|
||||||
|
{
|
||||||
|
ToastNotification.ShowCustom("Выберите накладную для экспорта.", Color.DarkOrange, Color.White);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = dgvInvoice.SelectedRows[0];
|
||||||
|
var invoiceId = row.Cells["ИД накладной"]?.Value?.ToString() ?? string.Empty;
|
||||||
|
var invoiceNumber = row.Cells["Номер"]?.Value?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(invoiceId))
|
||||||
|
{
|
||||||
|
ToastNotification.ShowCustom("Не удалось определить накладную для экспорта.", Color.DarkOrange, Color.White);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exportService = new InvoiceExportService();
|
||||||
|
var filePath = exportService.ExportInvoice(invoiceId, FindForm());
|
||||||
|
ToastNotification.ShowSuccess($"Накладная {invoiceNumber} сохранена:\n{filePath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AppDebugLog.Error("Invoices", $"Ошибка экспорта накладной {invoiceNumber}", ex);
|
||||||
|
ToastNotification.ShowError($"Не удалось экспортировать накладную: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e)
|
private void txtInvoiceNumber_KeyPress(object sender, KeyPressEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.KeyChar == (char)Keys.Enter)
|
if (e.KeyChar == (char)Keys.Enter)
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
this.dgvOrder = new Elfisa.UI.Controls.ModernDataGridView();
|
this.dgvOrder = new Elfisa.UI.Controls.ModernDataGridView();
|
||||||
this.lblSumOfOrder = new System.Windows.Forms.Label();
|
this.lblSumOfOrder = new System.Windows.Forms.Label();
|
||||||
this.btnEraseOrder = new Elfisa.UI.Controls.ModernButton();
|
this.btnEraseOrder = new Elfisa.UI.Controls.ModernButton();
|
||||||
|
this.btnDeleteGood = new Elfisa.UI.Controls.ModernButton();
|
||||||
this.btnSendOrder = new Elfisa.UI.Controls.ModernButton();
|
this.btnSendOrder = new Elfisa.UI.Controls.ModernButton();
|
||||||
this.button1 = new Elfisa.UI.Controls.ModernButton();
|
this.button1 = new Elfisa.UI.Controls.ModernButton();
|
||||||
this.lblMinimalSumOfOrder = new System.Windows.Forms.Label();
|
this.lblMinimalSumOfOrder = new System.Windows.Forms.Label();
|
||||||
@ -93,6 +94,18 @@
|
|||||||
this.btnEraseOrder.Text = "Очистить корзину";
|
this.btnEraseOrder.Text = "Очистить корзину";
|
||||||
this.btnEraseOrder.Click += new System.EventHandler(this.btnEraseOrder_Click);
|
this.btnEraseOrder.Click += new System.EventHandler(this.btnEraseOrder_Click);
|
||||||
//
|
//
|
||||||
|
// btnDeleteGood
|
||||||
|
//
|
||||||
|
this.btnDeleteGood.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||||
|
this.btnDeleteGood.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold);
|
||||||
|
this.btnDeleteGood.Location = new System.Drawing.Point(538, 36);
|
||||||
|
this.btnDeleteGood.Margin = new System.Windows.Forms.Padding(3, 2, 3, 2);
|
||||||
|
this.btnDeleteGood.Name = "btnDeleteGood";
|
||||||
|
this.btnDeleteGood.Size = new System.Drawing.Size(260, 38);
|
||||||
|
this.btnDeleteGood.TabIndex = 17;
|
||||||
|
this.btnDeleteGood.Text = "Удалить товар";
|
||||||
|
this.btnDeleteGood.Click += new System.EventHandler(this.btnDeleteGood_Click);
|
||||||
|
//
|
||||||
// btnSendOrder
|
// btnSendOrder
|
||||||
//
|
//
|
||||||
this.btnSendOrder.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
this.btnSendOrder.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
||||||
@ -184,6 +197,7 @@
|
|||||||
this.Controls.Add(this.btnSaveAndSendOrder);
|
this.Controls.Add(this.btnSaveAndSendOrder);
|
||||||
this.Controls.Add(this.lblMinimalSumOfOrder);
|
this.Controls.Add(this.lblMinimalSumOfOrder);
|
||||||
this.Controls.Add(this.lblSumOfOrder);
|
this.Controls.Add(this.lblSumOfOrder);
|
||||||
|
this.Controls.Add(this.btnDeleteGood);
|
||||||
this.Controls.Add(this.btnEraseOrder);
|
this.Controls.Add(this.btnEraseOrder);
|
||||||
this.Controls.Add(this.btnSendOrder);
|
this.Controls.Add(this.btnSendOrder);
|
||||||
this.Controls.Add(this.button1);
|
this.Controls.Add(this.button1);
|
||||||
@ -205,6 +219,7 @@
|
|||||||
private Elfisa.UI.Controls.ModernDataGridView dgvOrder;
|
private Elfisa.UI.Controls.ModernDataGridView dgvOrder;
|
||||||
private System.Windows.Forms.Label lblSumOfOrder;
|
private System.Windows.Forms.Label lblSumOfOrder;
|
||||||
private Elfisa.UI.Controls.ModernButton btnEraseOrder;
|
private Elfisa.UI.Controls.ModernButton btnEraseOrder;
|
||||||
|
private Elfisa.UI.Controls.ModernButton btnDeleteGood;
|
||||||
private Elfisa.UI.Controls.ModernButton btnSendOrder;
|
private Elfisa.UI.Controls.ModernButton btnSendOrder;
|
||||||
private Elfisa.UI.Controls.ModernButton button1;
|
private Elfisa.UI.Controls.ModernButton button1;
|
||||||
private System.Windows.Forms.Label lblMinimalSumOfOrder;
|
private System.Windows.Forms.Label lblMinimalSumOfOrder;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ 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
|
||||||
@ -39,7 +40,24 @@ namespace Электронная_Фармация.UserControls
|
|||||||
|
|
||||||
public void loadDataAboutOrderV2(string Supplier)
|
public void loadDataAboutOrderV2(string Supplier)
|
||||||
{
|
{
|
||||||
string qShowMeOrderItems = "select * from TempOrderItems";
|
var consigneeSafe = (_consigneeName ?? string.Empty).Replace("'", "''");
|
||||||
|
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";
|
||||||
|
|
||||||
@ -53,7 +71,8 @@ namespace Электронная_Фармация.UserControls
|
|||||||
|
|
||||||
if (Supplier != string.Empty && Supplier != "Корзина")
|
if (Supplier != string.Empty && Supplier != "Корзина")
|
||||||
{
|
{
|
||||||
qShowMeOrderItems += $" where [SupplierName] = '{Supplier}'";
|
var supplierSafe = Supplier.Replace("'", "''");
|
||||||
|
qShowMeOrderItems += $" and [SupplierName] = '{supplierSafe}'";
|
||||||
}
|
}
|
||||||
|
|
||||||
SQLiteCommand cmdShowMeTempOrderItems = new SQLiteCommand(qShowMeOrderItems, conTempOrder);
|
SQLiteCommand cmdShowMeTempOrderItems = new SQLiteCommand(qShowMeOrderItems, conTempOrder);
|
||||||
@ -248,22 +267,103 @@ and ConsigneesName = '{consigneeName}'
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void btnDeleteGood_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (dgvOrder.SelectedRows.Count == 0 || dgvOrder.CurrentRow == null)
|
||||||
|
{
|
||||||
|
ToastNotification.ShowCustom("Выберите наименование в корзине для удаления.", Color.DarkOrange, Color.White);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var row = dgvOrder.SelectedRows[0];
|
||||||
|
string idPriceListItem = null;
|
||||||
|
|
||||||
|
if (dgvOrder.Columns.Contains("id_PriceList_Item"))
|
||||||
|
{
|
||||||
|
idPriceListItem = row.Cells["id_PriceList_Item"]?.Value?.ToString();
|
||||||
|
}
|
||||||
|
else if (row.Cells.Count > 11)
|
||||||
|
{
|
||||||
|
idPriceListItem = row.Cells[11]?.Value?.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(idPriceListItem))
|
||||||
|
{
|
||||||
|
ToastNotification.ShowCustom("Не удалось определить позицию для удаления.", Color.DarkOrange, Color.White);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var drugName = dgvOrder.Columns.Contains("Наименование")
|
||||||
|
? row.Cells["Наименование"]?.Value?.ToString()
|
||||||
|
: row.Cells.Count > 3
|
||||||
|
? row.Cells[3]?.Value?.ToString()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
DeleteCartItem(idPriceListItem, drugName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteCartItem(string idPriceListItem, string drugName)
|
||||||
|
{
|
||||||
|
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 qDeleteFromTempOrder = $"delete from [TempOrderItems] where [id_PriceList_Item] = '{safeId}' and [ConsigneeName] = '{consigneeSafe}'";
|
||||||
|
|
||||||
|
using (var sqlCon = new SQLiteConnection(connectionStringToLocalDB))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sqlCon.Open();
|
||||||
|
using (var cmdClear = new SQLiteCommand(qClearPriceList, sqlCon))
|
||||||
|
using (var cmdDelete = new SQLiteCommand(qDeleteFromTempOrder, sqlCon))
|
||||||
|
{
|
||||||
|
cmdClear.ExecuteNonQuery();
|
||||||
|
cmdDelete.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(drugName))
|
||||||
|
{
|
||||||
|
ToastNotification.ShowSuccess($"Удалено: {drugName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ToastNotification.ShowError($"Ошибка удаления товара: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
sqlCon.Close();
|
||||||
|
loadDataAboutOrderV2(_supplierName);
|
||||||
|
ParentForm?.loadPriceListV2();
|
||||||
|
ParentForm?.loadTempOrderItems();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void EraseOrder(string Supplier)
|
void EraseOrder(string Supplier)
|
||||||
{
|
{
|
||||||
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 (Supplier != string.Empty)
|
if (supplierSafe != string.Empty)
|
||||||
{
|
{
|
||||||
qDeletePriceListItemForASupplier = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where [id_PriceList_Item] in (select [id_PriceList_Item] from [TempOrderItems] where [SupplierName] = '{Supplier}')";
|
qDeletePriceListItemForASupplier =
|
||||||
qDeleteTempOrderForASupplier = $"delete from [TempOrderItems] where [SupplierName] = '{Supplier}'";
|
$"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null where [id_PriceList_Item] in " +
|
||||||
|
$"(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 = $"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null";
|
qDeletePriceListItemForASupplier =
|
||||||
qDeleteTempOrderForASupplier = $"delete from [TempOrderItems]";
|
$"update [PriceList] set [Zakaz] = null, [SummaZakaza] = null " +
|
||||||
|
$"where [id_PriceList_Item] in (select [id_PriceList_Item] from [TempOrderItems] where [ConsigneeName] = '{consigneeSafe}')";
|
||||||
|
qDeleteTempOrderForASupplier =
|
||||||
|
$"delete from [TempOrderItems] where [ConsigneeName] = '{consigneeSafe}'";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -351,10 +451,18 @@ 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("'", "''");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -393,6 +501,7 @@ select
|
|||||||
SupplierName
|
SupplierName
|
||||||
from TempOrderItems
|
from TempOrderItems
|
||||||
where zakaz > 0
|
where zakaz > 0
|
||||||
|
and [ConsigneeName] = '{consigneeSafe}'
|
||||||
{filterAddon}
|
{filterAddon}
|
||||||
group by SupplierName
|
group by SupplierName
|
||||||
";
|
";
|
||||||
@ -445,9 +554,12 @@ group by SupplierName
|
|||||||
|
|
||||||
void createOrderForNextSuppliers(string SupName)
|
void createOrderForNextSuppliers(string SupName)
|
||||||
{
|
{
|
||||||
string OrderNumber = $"EP-{DateTime.Now:yyyyMMdd-HHmmss}-{Guid.NewGuid().ToString("N").Substring(0, 6)}";
|
// Глобальный номер EX-0000001… (сервер / локальный fallback).
|
||||||
string OrderDate = DateTime.Now.ToString("dd.MM.yyyy");
|
string OrderNumber = OrderNumberHelper.AllocateForNewOrder();
|
||||||
|
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))
|
||||||
{
|
{
|
||||||
@ -460,14 +572,16 @@ select
|
|||||||
count(*)
|
count(*)
|
||||||
from TempOrderItems
|
from TempOrderItems
|
||||||
where Zakaz > 0
|
where Zakaz > 0
|
||||||
and SupplierName = '{SupName.Replace("'", "''")}'
|
and SupplierName = '{supNameSafe}'
|
||||||
|
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 = '{SupName.Replace("'", "''")}'
|
and SupplierName = '{supNameSafe}'
|
||||||
|
and [ConsigneeName] = '{consigneeSafe}'
|
||||||
";
|
";
|
||||||
string qShowMeMinimalSumOfOrder = $@"
|
string qShowMeMinimalSumOfOrder = $@"
|
||||||
select
|
select
|
||||||
@ -477,7 +591,7 @@ where SuppliersName = '{SupName.Replace("'", "''")}'
|
|||||||
";
|
";
|
||||||
|
|
||||||
string qDisposeTempOrder = $@"
|
string qDisposeTempOrder = $@"
|
||||||
delete from TempOrderItems where SupplierName = '{SupName}'
|
delete from TempOrderItems where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}'
|
||||||
";
|
";
|
||||||
|
|
||||||
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
string connectionStringToLocalDB = AppConfig.SqliteConnectionString;
|
||||||
@ -522,12 +636,13 @@ select
|
|||||||
'{OrderDate}',
|
'{OrderDate}',
|
||||||
supplier_id,
|
supplier_id,
|
||||||
SupplierName,
|
SupplierName,
|
||||||
(select count(*) from TempOrderItems where SupplierName = '{SupName.Replace("'", "''")}'),
|
(select count(*) from TempOrderItems where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}'),
|
||||||
'{sumOrder.ToString(System.Globalization.CultureInfo.InvariantCulture)}',
|
'{sumOrder.ToString(System.Globalization.CultureInfo.InvariantCulture)}',
|
||||||
'НОВЫЙ',
|
'НОВЫЙ',
|
||||||
'{_consigneeName.Replace("'", "''")}'
|
'{_consigneeName.Replace("'", "''")}'
|
||||||
from TempOrderItems
|
from TempOrderItems
|
||||||
where SupplierName = '{SupName.Replace("'", "''")}'
|
where SupplierName = '{supNameSafe}'
|
||||||
|
and [ConsigneeName] = '{consigneeSafe}'
|
||||||
group by supplier_id, SupplierName
|
group by supplier_id, SupplierName
|
||||||
";
|
";
|
||||||
|
|
||||||
@ -544,7 +659,7 @@ select
|
|||||||
[ExpiryPeriod],
|
[ExpiryPeriod],
|
||||||
[supplier_price_id]
|
[supplier_price_id]
|
||||||
from TempOrderItems
|
from TempOrderItems
|
||||||
where SupplierName = '{SupName}'
|
where SupplierName = '{supNameSafe}' and [ConsigneeName] = '{consigneeSafe}'
|
||||||
";
|
";
|
||||||
|
|
||||||
#region старыеЗапросыСозданияЗаказа
|
#region старыеЗапросыСозданияЗаказа
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
namespace Электронная_Фармация.UserControls
|
namespace Электронная_Фармация.UserControls
|
||||||
{
|
{
|
||||||
partial class UCPriceList
|
partial class UCPriceList
|
||||||
{
|
{
|
||||||
@ -36,7 +36,6 @@
|
|||||||
this.TLPPriceList = new System.Windows.Forms.TableLayoutPanel();
|
this.TLPPriceList = new System.Windows.Forms.TableLayoutPanel();
|
||||||
this.dgvPriceList = new Elfisa.UI.Controls.ModernDataGridView();
|
this.dgvPriceList = new Elfisa.UI.Controls.ModernDataGridView();
|
||||||
this.panel1 = new System.Windows.Forms.Panel();
|
this.panel1 = new System.Windows.Forms.Panel();
|
||||||
this.btnDeleteGood = new Elfisa.UI.Controls.ModernButton();
|
|
||||||
this.checkUsePercentageAsDefault = new System.Windows.Forms.CheckBox();
|
this.checkUsePercentageAsDefault = new System.Windows.Forms.CheckBox();
|
||||||
this.lblPriceForGood = new System.Windows.Forms.Label();
|
this.lblPriceForGood = new System.Windows.Forms.Label();
|
||||||
this.numericPercent = new System.Windows.Forms.NumericUpDown();
|
this.numericPercent = new System.Windows.Forms.NumericUpDown();
|
||||||
@ -100,7 +99,7 @@
|
|||||||
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 = 2;
|
this.SplitPriceList.SplitterWidth = 6;
|
||||||
this.SplitPriceList.TabIndex = 0;
|
this.SplitPriceList.TabIndex = 0;
|
||||||
//
|
//
|
||||||
// TLPPriceList
|
// TLPPriceList
|
||||||
@ -149,7 +148,6 @@
|
|||||||
//
|
//
|
||||||
// panel1
|
// panel1
|
||||||
//
|
//
|
||||||
this.panel1.Controls.Add(this.btnDeleteGood);
|
|
||||||
this.panel1.Controls.Add(this.checkUsePercentageAsDefault);
|
this.panel1.Controls.Add(this.checkUsePercentageAsDefault);
|
||||||
this.panel1.Controls.Add(this.lblPriceForGood);
|
this.panel1.Controls.Add(this.lblPriceForGood);
|
||||||
this.panel1.Controls.Add(this.numericPercent);
|
this.panel1.Controls.Add(this.numericPercent);
|
||||||
@ -163,17 +161,6 @@
|
|||||||
this.panel1.Size = new System.Drawing.Size(1039, 54);
|
this.panel1.Size = new System.Drawing.Size(1039, 54);
|
||||||
this.panel1.TabIndex = 1;
|
this.panel1.TabIndex = 1;
|
||||||
//
|
//
|
||||||
// btnDeleteGood
|
|
||||||
//
|
|
||||||
this.btnDeleteGood.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
|
|
||||||
this.btnDeleteGood.Font = new System.Drawing.Font("Segoe UI Semibold", 12F, System.Drawing.FontStyle.Bold);
|
|
||||||
this.btnDeleteGood.Location = new System.Drawing.Point(8, 62);
|
|
||||||
this.btnDeleteGood.Name = "btnDeleteGood";
|
|
||||||
this.btnDeleteGood.Size = new System.Drawing.Size(360, 34);
|
|
||||||
this.btnDeleteGood.TabIndex = 13;
|
|
||||||
this.btnDeleteGood.Text = "Удалить товар (BACKSPACE)";
|
|
||||||
this.btnDeleteGood.Click += new System.EventHandler(this.btnDeleteGood_Click);
|
|
||||||
//
|
|
||||||
// checkUsePercentageAsDefault
|
// checkUsePercentageAsDefault
|
||||||
//
|
//
|
||||||
this.checkUsePercentageAsDefault.AutoSize = true;
|
this.checkUsePercentageAsDefault.AutoSize = true;
|
||||||
@ -514,6 +501,5 @@
|
|||||||
private Elfisa.UI.Controls.ModernButton btnClearSupplierFilter;
|
private Elfisa.UI.Controls.ModernButton btnClearSupplierFilter;
|
||||||
private System.Windows.Forms.CheckBox checkUsePercentageAsDefault;
|
private System.Windows.Forms.CheckBox checkUsePercentageAsDefault;
|
||||||
private System.Windows.Forms.Timer timerForResetSearch;
|
private System.Windows.Forms.Timer timerForResetSearch;
|
||||||
private Elfisa.UI.Controls.ModernButton btnDeleteGood;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -109,6 +109,7 @@ namespace Электронная_Фармация.UserControls
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
AppDebugLog.Error("Refusals", "Не удалось загрузить отказы с API", ex);
|
||||||
ToastNotification.ShowError($"Не удалось загрузить отказы: {ex.Message}");
|
ToastNotification.ShowError($"Не удалось загрузить отказы: {ex.Message}");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -123,12 +124,7 @@ 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();
|
||||||
@ -185,26 +181,6 @@ 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 @"
|
||||||
@ -236,26 +212,5 @@ 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;";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
318
src/ElectronicPharmacy/UserControls/UCReports.cs
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/Elfisa.Avalonia/App.axaml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<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>
|
||||||
35
src/Elfisa.Avalonia/App.axaml.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/Elfisa.Avalonia/Assets/avalonia-logo.ico
Normal file
|
After Width: | Height: | Size: 172 KiB |
38
src/Elfisa.Avalonia/Elfisa.Avalonia.csproj
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<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>
|
||||||
24
src/Elfisa.Avalonia/Program.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
73
src/Elfisa.Avalonia/README.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# ЭльФиСА — кросс-платформенный клиент (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).
|
||||||
193
src/Elfisa.Avalonia/Styles/Elfisa.axaml
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
<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>
|
||||||
37
src/Elfisa.Avalonia/ViewLocator.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Elfisa.Avalonia/ViewModels/CartService.cs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
48
src/Elfisa.Avalonia/ViewModels/DirectoryViewModel.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/Elfisa.Avalonia/ViewModels/InvoicesViewModel.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/Elfisa.Avalonia/ViewModels/LoadPriceViewModel.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Elfisa.Avalonia/ViewModels/LoginViewModel.cs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
165
src/Elfisa.Avalonia/ViewModels/OrdersViewModel.cs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
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 = "Черновик удалён.";
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Elfisa.Avalonia/ViewModels/PlaceholderPageViewModel.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
281
src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/Elfisa.Avalonia/ViewModels/ReportsViewModel.cs
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
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 { /* открыть не удалось — путь показан в статусе */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Elfisa.Avalonia/ViewModels/RootViewModel.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Elfisa.Avalonia/ViewModels/SettingsViewModel.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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 { /* локации не критичны для настроек */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
7
src/Elfisa.Avalonia/ViewModels/ViewModelBase.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Elfisa.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public abstract class ViewModelBase : ObservableObject
|
||||||
|
{
|
||||||
|
}
|
||||||
56
src/Elfisa.Avalonia/Views/DirectoryView.axaml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<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>
|
||||||