From 2659de14b923f2d2301a678a11fe311b98cf0afa Mon Sep 17 00:00:00 2001 From: Exest Date: Wed, 5 Aug 2026 15:22:44 +0300 Subject: [PATCH] Release v1.0.21: consignees from web, remove test pharmacies. --- .gitignore | 5 + installer/ElfisaPharmacy.iss | 110 ++++++ installer/README.md | 24 ++ installer/assets/elfisa-logo.png | Bin 0 -> 3360 bytes installer/assets/elfisa.ico | Bin 0 -> 7693 bytes installer/assets/wizard-side.bmp | Bin 0 -> 206038 bytes installer/assets/wizard-small.bmp | Bin 0 -> 12154 bytes installer/license.txt | 18 + installer/welcome.txt | 20 + scripts/build-installer.ps1 | 48 +++ src/ElectronicPharmacy/Classes/ApiClient.cs | 45 +++ src/ElectronicPharmacy/Classes/AppConfig.cs | 102 +++++- src/ElectronicPharmacy/Classes/AppDebugLog.cs | 5 +- .../Classes/ConsigneeHelper.cs | 125 +++++++ .../Classes/UiThemeHelper.cs | 25 +- .../ElectronicPharmacy.csproj | 4 + src/ElectronicPharmacy/Form1.Designer.cs | 7 +- src/ElectronicPharmacy/Form1.UiShell.cs | 90 ++++- .../Forms/FCheckDatabaseIntegrary.cs | 77 +--- src/ElectronicPharmacy/Forms/FConsignees.cs | 156 +++----- .../HelpForms/HF_DownloadDataFromServer.cs | 15 + .../Properties/AssemblyInfo.cs | 41 ++- .../Properties/Settings.Designer.cs | 12 + .../Properties/Settings.settings | 3 + src/ElectronicPharmacy/Resources/elfisa.ico | Bin 0 -> 7693 bytes .../UserControls/UCPriceList.Designer.cs | 4 +- .../UserControls/UCPriceList.cs | 210 +++++++++-- src/Elfisa.UI/Controls/ModernAppHeader.cs | 124 +++++-- src/Elfisa.UI/Controls/ModernButton.cs | 6 +- src/Elfisa.UI/Controls/ModernButtonStyles.cs | 5 +- .../Controls/ModernDocumentTabStrip.cs | 31 +- .../Controls/ModernSplitContainer.cs | 4 +- src/Elfisa.UI/Controls/ModernTitleBar.cs | 346 ++++++++++++++++++ src/Elfisa.UI/Controls/ModernUploadPanel.cs | 61 +-- .../Helpers/SkillGradientButtonPaint.cs | 34 +- src/Elfisa.UI/Helpers/WindowChromeHelper.cs | 37 +- src/Elfisa.UI/Theming/ClinicalProPalette.cs | 44 +-- src/Elfisa.UI/Theming/ThemeManager.cs | 4 +- 38 files changed, 1471 insertions(+), 371 deletions(-) create mode 100644 installer/ElfisaPharmacy.iss create mode 100644 installer/README.md create mode 100644 installer/assets/elfisa-logo.png create mode 100644 installer/assets/elfisa.ico create mode 100644 installer/assets/wizard-side.bmp create mode 100644 installer/assets/wizard-small.bmp create mode 100644 installer/license.txt create mode 100644 installer/welcome.txt create mode 100644 scripts/build-installer.ps1 create mode 100644 src/ElectronicPharmacy/Resources/elfisa.ico create mode 100644 src/Elfisa.UI/Controls/ModernTitleBar.cs diff --git a/.gitignore b/.gitignore index e6709b5..5314b94 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ Desktop.ini efClient.db *.db-journal **/user.config + +packages/ +installer/output/ +dist/ + diff --git a/installer/ElfisaPharmacy.iss b/installer/ElfisaPharmacy.iss new file mode 100644 index 0000000..430daf5 --- /dev/null +++ b/installer/ElfisaPharmacy.iss @@ -0,0 +1,110 @@ +; ЭльФиСА — установщик «Электронная Фармация» +; Кодировка: UTF-8 with BOM + +#define MyAppName "Электронная Фармация" +#define MyAppBrand "ЭльФиСА" +#define MyAppVersion "1.0.20" +#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] +; Основной выпуск Release (без отладочных PDB и XML-документации) +Source: "..\src\ElectronicPharmacy\bin\Release\*.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\src\ElectronicPharmacy\bin\Release\*.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\src\ElectronicPharmacy\bin\Release\*.config"; DestDir: "{app}"; Flags: ignoreversion +Source: "..\src\ElectronicPharmacy\bin\Release\x86\*"; DestDir: "{app}\x86"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist +Source: "..\src\ElectronicPharmacy\bin\Release\x64\*"; DestDir: "{app}\x64"; Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist +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" \ No newline at end of file diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000..1ae2bdd --- /dev/null +++ b/installer/README.md @@ -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). diff --git a/installer/assets/elfisa-logo.png b/installer/assets/elfisa-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..471db312f509e413fa4c16df007c1238323e81f5 GIT binary patch literal 3360 zcmdT{`#;oKA3xujiJ{!Xh%qut+T3DFu^2Qag=(v%HaaO`wV%omjME!7*`6PQgR02AvK-~~WK;aWxH8U$-3?LLwM zfOhAX0d*(VL<6wyzTYmdLubQg-uA|sP2jsfDb?K<`;G^?%lj(#eGJ${$oQ~T7n^0Y zP2D)^!-Y$G{!7TubW}6-4!wY{$ux`S;3_h6kLRGBf^`q9jq*X+H*<{s>?^;o)i@tI z(=*-EHMp{ksF%+jC_#v6jeAt8-<@ zyi*lT9YI0n!*%^F=I!H?Y-OnXii92Bv?P&5&pr98L$wg@5WpJgEmYYnGH?zk$I~4< z(jGJC#tLiiKMD`@XjUx`crInsKf0A9T{_mrZegxm={U>@EXAtqFQOsbHJ6c$*v~Im zjIq2ALxi$y!poQD?kmaq7WmCJyG4@xNJ~KE>b&~AaOYK*fUyjj{c>&}qxMQmViMgE zg^A$O+wUeG!Y5D(jSc(P(QCQH5@~8w1D4M>ia;~s4i z_~3;EJy?6Ph&_Gq45#29RG|3W%l>ashi<99wBRYtlWT$!LZOcYJI=7Bq-$Rf5^4%T zk6-4&^krk}n4O5_<&KHamaF|Za@A&UW~NB_Bp$=H@|ANaQ|70$xI0lo6hRsJ^41+1 zJJJ;;EJ_H^A4$il1*-qD5opo)4oq=maI<-2v#Oo|)odQS-Jah)^h(}8{#Ut3IyW7Qi|B+7LTtEN^xp;L3@D%;M0oOEF?S7#-a@T-~YMKJav#hwkd* z29eo$>B4#7e|tq%=_1DAW$#vJw+SjX=~FxtFXGl;)Pu=YbF9+m)VhXywp)yy&2@?3 zy#LaC+#ye9erjmiZ0d4Kcg`nx70Wk&W4-e0NUKS^piP-X9B7Wc+hfKH48lZ|_;ZwX zQnVSbzpZf%nN&w)-YvlL?V01O`D_zsRDF5b6CCe5D(0P)mM$f+xPb_^erLcu4<27# zc=i}$Q8l6W;i?1kMIe@+axL+b6%cQ=*bS_m|8lAu-wUe5!(gcrtbpk7cbUMfXVr%9 z0p1LJ-UQTBJT>dJ`J%juP8W(bCSvBH)@K%>cMtI1P&4=)sI*T~IvgUDDs| zA3*bjqupz3V1Gt6Lji;0=1;Wi(NTJ!TZ3Gu3on{kV~$`VPR_(X%)}#*lw@}5ioY$2 z+fAmpy@nv#zTO`%KJc?Pf`oknntDn7r?cly`}q-WQI=obxHBy6C}bsQ3?>Y+L=3yg zjH$?n0Zt8A^5lZ5UCx@2$;LRNk_(LDe!=0EjxCRh@;;j7D~rEef0^p`P)FW>I^=NG zt@r!t4)4( zB&8aV^ywv?5goRR{J{4K)Wt_AJCtzAmRx$px5Ie#Ydr?4h}?Yq(Jj%Wbg3dd$UGhY zvCw{U`)BEy*h?P3)uE6PUxHJFtb1XYvq&vz^t8(%!rI4%6xIr!G$_|hn?5Hq!LC95R`(w#EB-few^j3YkP1uKem0^~( zJOm*RHe%#3k|dr0R1!D^6XybQsWs zjb~CY3I#Z8vr#m>7Qqm5zxwSL+fh-5~!`ERFD?-)CgqhqIKe;Ymm zIlgI?rR91MZ9M;sM5D#$yJBcZ+KTVPIcH0-FzJ-stmV|S4i?Wm!EimgDWP^~i`(C6 zhLKSF3uAI)!cpHuAu9Lt!f?$w?>j6XK>KI#YG@6zyM2mksZk3?yvwlN#AzdFY8{rF zHfY24vgi(lu-ou|MR_Zv7G}mzj=VmG8-89SMPlyibHCm zOniXc{|KYOrwb;Oh_*^b6w8&f0ZxoBUPke)8_=ek#v%&PGQ{?@I(2?F)$U-$U%xmDEF`EDw&mj86T!MXb4}ZTLA~GKD2u zh-YNZU{CLz7rv ztY5asslF)R=Y?c6ypMFDe)n$A!B*r zuF2|+hSA5W2n;v-{Zvq=%K+}=A@<;Cy+GDvwwNa-whvl@Z6`wf&Su0l9p$B@oGuMl z^{EP!yoQ&W9R+cUZ4|H}>0GTacyiJseR&FLhdVu@()~ZWzlr?4143G5`;mDq1D>ot zc*8?Fvp&`sOI|C_PENbW51t)OBKQ^NLzXNllG5#uzB(xM^A0p3I#B$wyl|;G32!-JC^Qg$PVwok~cEdLW|E$ zuT)i9yVqC^7Dy3!?>>%hJNC0I^46RyjfbKa0$cvggPV5Pz4mdI<3=42TqH<*>%+|o z+7`Uw6k)y)tb+@%!yh*b)x)Rke238Qbzw;Km1UO2-0jZ2OMg2xdRky2U{dhe>CaiD zYIXU8ff|p@bDcgwEpQGyKWZ|B`l^i@erIz^xw2jl))R63whANXh33H(Vk3M5t-q{` zRM9~|_WSxzyp;1N+m|UlcEX#y_V=ynE@2o8z0%`x(V>Nq_y=NEW7;;ZLG2a zkNLUOqx!3?i#m{FCFGltQys|_tcAcuJ;2BPTSGkW@G9Y1+S1Fz!ciRZs{wwLz+Lye HBQE?8q2<68 literal 0 HcmV?d00001 diff --git a/installer/assets/elfisa.ico b/installer/assets/elfisa.ico new file mode 100644 index 0000000000000000000000000000000000000000..20dd21e396fe6f40e0d1f8abe5fa700d6f8032a6 GIT binary patch literal 7693 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 bnf1Txa=qd50e7eW_Tc6IIoALE|Iz;d?4;@$ literal 0 HcmV?d00001 diff --git a/installer/assets/wizard-side.bmp b/installer/assets/wizard-side.bmp new file mode 100644 index 0000000000000000000000000000000000000000..30655e934c1888df498de526af6e5ae6b693daff GIT binary patch literal 206038 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 literal 0 HcmV?d00001 diff --git a/installer/assets/wizard-small.bmp b/installer/assets/wizard-small.bmp new file mode 100644 index 0000000000000000000000000000000000000000..d822bbe02a906673174ccf90cddbd5aced7f366f GIT binary patch literal 12154 zcmeHM%`0tD6uFb9|exyymRp$f6M(kjj`Q{v{|q4@v$ z@b#dK`qho37#yw2nYeFay8as$8J8}9dv_{+&o*W)M(7E;(8=z#?&@Y=43Af56~>>x zYQ^Kzbtqhf|M}%s%*;1s1wL?zp zJ1jO^cRD}uKj|YZHrzYpIikw@y#s{Rn+-nFUSSXY#*{P`De2N{YWrOn-p_fJ4gGpP zIhDzO>E^SkGunU1=oF*qWQ6}4o2+&8U%D|k*B!|sE9Z=GQHphg*cBZPwGN!OajzjP zx)ia5X?(ziyOeG{+jY>NAAYJjlFNwq!^Uo=@#Fl#zWBADgbVMq@JYxnTMn?omV>uQQAS*9+i#h|#q;Fu_eItn>yA0V9AFMG2bcrQ J0p>t~cmveJ=~Vy# literal 0 HcmV?d00001 diff --git a/installer/license.txt b/installer/license.txt new file mode 100644 index 0000000..f4c81f8 --- /dev/null +++ b/installer/license.txt @@ -0,0 +1,18 @@ +ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ +Электронная Фармация (ЭльФиСА) + +1. Программа предоставляется для использования в рамках договора + с поставщиком услуг PharmData / 24pharmdata. + +2. Запрещается передавать третьим лицам учётные данные доступа, + а также модифицировать клиент способом, нарушающим безопасность + обмена данными. + +3. Пользователь несёт ответственность за корректность оформляемых + заказов и за сохранность локальных данных на рабочем месте. + +4. Устанавливая программу, вы подтверждаете согласие с условиями + использования сервиса и политикой обработки данных, действующими + у оператора сервиса. + +При возникновении вопросов обратитесь в службу поддержки. diff --git a/installer/welcome.txt b/installer/welcome.txt new file mode 100644 index 0000000..612fb13 --- /dev/null +++ b/installer/welcome.txt @@ -0,0 +1,20 @@ +ЭльФиСА — Электронная Фармация +================================ + +Добро пожаловать в установку настольного клиента «Электронная Фармация». + +Программа предназначена для работы аптек с прайс-листами поставщиков, +заказами, накладными и обменом данными с сервером PharmData. + +Скачать установщик всегда можно по адресу: + https://cdn.24pharmdata.ru + +Установщик: + • скопирует программу в выбранную папку; + • предложит ярлык в меню «Пуск»; + • по желанию создаст ярлык на рабочем столе. + +Рекомендуется закрыть запущенную «Электронную Фармацию» перед установкой. +После установки при первом запуске выполните вход в систему. + +© PharmData / ЭльФиСА diff --git a/scripts/build-installer.ps1 b/scripts/build-installer.ps1 new file mode 100644 index 0000000..d26040d --- /dev/null +++ b/scripts/build-installer.ps1 @@ -0,0 +1,48 @@ +#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" +} + +if (-not $SkipBuild) { + Write-Host "Сборка Release..." -ForegroundColor Cyan + dotnet build "src\ElectronicPharmacy\ElectronicPharmacy.csproj" -c Release + if ($LASTEXITCODE -ne 0) { throw "Сборка приложения завершилась с ошибкой" } +} + +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" diff --git a/src/ElectronicPharmacy/Classes/ApiClient.cs b/src/ElectronicPharmacy/Classes/ApiClient.cs index 33ae71c..e6c263d 100644 --- a/src/ElectronicPharmacy/Classes/ApiClient.cs +++ b/src/ElectronicPharmacy/Classes/ApiClient.cs @@ -374,6 +374,30 @@ namespace Электронная_Фармация.Classes } } + public async Task> GetBuyerLocationsAsync() + { + EnsureAuthenticated(); + var responseBody = await SendAuthorizedGetAsync("/api/buyer/locations", "загрузку грузополучателей"); + try + { + using (var doc = JsonDocument.Parse(responseBody)) + { + if (doc.RootElement.ValueKind == JsonValueKind.Array) + { + return JsonSerializer.Deserialize>(responseBody, JsonOptions) + ?? new List(); + } + } + + return new List(); + } + catch (Exception ex) + { + AppDebugLog.Error("ApiClient", "Не удалось разобрать грузополучателей", ex); + throw new HttpRequestException($"Некорректный ответ сервера при загрузке грузополучателей: {ex.Message}", ex); + } + } + public async Task GetBuyerOrderByIdAsync(string orderId) { EnsureAuthenticated(); @@ -734,6 +758,27 @@ namespace Электронная_Фармация.Classes public List Orders { get; set; } } + public sealed class BuyerLocationDto + { + [JsonPropertyName("buyer_location_id")] + public string BuyerLocationID { get; set; } + + [JsonPropertyName("buyer_id")] + public string BuyerID { get; set; } + + [JsonPropertyName("address")] + public string Address { get; set; } + + [JsonPropertyName("region_id")] + public string RegionID { get; set; } + + [JsonPropertyName("is_default")] + public bool IsDefault { get; set; } + + [JsonPropertyName("region_name")] + public string RegionName { get; set; } + } + public sealed class BuyerOrderListItemDto { [JsonPropertyName("order_id")] diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs index 4bf6a89..96bae7c 100644 --- a/src/ElectronicPharmacy/Classes/AppConfig.cs +++ b/src/ElectronicPharmacy/Classes/AppConfig.cs @@ -12,11 +12,42 @@ namespace Электронная_Фармация.Classes public const string DefaultApiBaseUrl = "https://24pharmdata.ru"; public const string DefaultUpdateCheckBaseUrl = "https://git.24pharmdata.ru"; public const string DefaultUpdateRepo = "pharmdata/elfisa-pharmacy"; + /// Публичная раздача установщика (корень CDN отдаёт Setup.exe). + 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 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; + } + } + /// /// Replaces known dead API URLs in saved user settings. /// @@ -97,24 +128,77 @@ namespace Электронная_Фармация.Classes return url; } + /// + /// Writable per-user data (DB, logs). Program Files is read-only for normal users. + /// + 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 { get { + MigrateLegacySqliteDatabaseIfNeeded(); + return Path.Combine(UserDataDirectory, "efClient.db"); + } + } + + /// + /// One-time copy from install folder (old portable layout) into LocalAppData. + /// + private static void MigrateLegacySqliteDatabaseIfNeeded() + { + try + { + var target = Path.Combine(UserDataDirectory, "efClient.db"); + if (File.Exists(target)) + { + return; + } + var baseDir = AppDomain.CurrentDomain.BaseDirectory; - var root = Path.Combine(baseDir, "efClient.db"); - var data = Path.Combine(baseDir, "Data", "efClient.db"); - if (File.Exists(root)) + var candidates = new[] { - 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}"); } } diff --git a/src/ElectronicPharmacy/Classes/AppDebugLog.cs b/src/ElectronicPharmacy/Classes/AppDebugLog.cs index a9ac056..946f276 100644 --- a/src/ElectronicPharmacy/Classes/AppDebugLog.cs +++ b/src/ElectronicPharmacy/Classes/AppDebugLog.cs @@ -8,7 +8,7 @@ namespace Электронная_Фармация.Classes { /// /// File logger for API/auth/network debugging. - /// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable. + /// Logs are written to %LocalAppData%\ЭльФиСА\...\Logs\api-debug-YYYY-MM-DD.log. /// public static class AppDebugLog { @@ -33,8 +33,7 @@ namespace Электронная_Фармация.Classes } } - public static string LogDirectory => - Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + public static string LogDirectory => AppConfig.LogsDirectory; public static string CurrentLogFilePath => Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log"); diff --git a/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs b/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs index 19e75b2..9bd4818 100644 --- a/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs +++ b/src/ElectronicPharmacy/Classes/ConsigneeHelper.cs @@ -322,6 +322,131 @@ namespace Электронная_Фармация.Classes } } + /// + /// Подтягивает грузополучателей с сервера (BuyerLocation) в локальную таблицу Consignees. + /// Источник истины — веб-кабинет; локально только кэш + выбор активной аптеки. + /// + public static int SyncFromServer(IList 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; + } + /// /// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки. /// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется diff --git a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs index b8ab6d0..7561f60 100644 --- a/src/ElectronicPharmacy/Classes/UiThemeHelper.cs +++ b/src/ElectronicPharmacy/Classes/UiThemeHelper.cs @@ -125,15 +125,36 @@ namespace Электронная_Фармация.Classes } 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 } - else if (control.Tag as string == "chrome-child") + else if (tag == "chrome-child") { control.BackColor = theme.BackgroundPrimary; 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 { control.BackColor = theme.BackgroundSecondary; diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj index c3750a0..ca9ef57 100644 --- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj +++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj @@ -17,6 +17,7 @@ true + Resources\elfisa.ico AnyCPU @@ -353,4 +354,7 @@ + + + \ No newline at end of file diff --git a/src/ElectronicPharmacy/Form1.Designer.cs b/src/ElectronicPharmacy/Form1.Designer.cs index 92c74e5..236630d 100644 --- a/src/ElectronicPharmacy/Form1.Designer.cs +++ b/src/ElectronicPharmacy/Form1.Designer.cs @@ -1,4 +1,4 @@ -namespace Электронная_Фармация +namespace Электронная_Фармация { partial class ElectroPharmacy { @@ -381,8 +381,11 @@ this.ClientSize = new System.Drawing.Size(1089, 554); this.Controls.Add(this.bindControl); 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.MinimumSize = new System.Drawing.Size(900, 620); this.Name = "ElectroPharmacy"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; this.Text = "Электронная Фармация"; diff --git a/src/ElectronicPharmacy/Form1.UiShell.cs b/src/ElectronicPharmacy/Form1.UiShell.cs index fb054bb..89c3382 100644 --- a/src/ElectronicPharmacy/Form1.UiShell.cs +++ b/src/ElectronicPharmacy/Form1.UiShell.cs @@ -23,6 +23,7 @@ namespace Электронная_Фармация private ToastService _toastService; private LoadingOverlay _loadingOverlay; private ModernAppHeader _appHeader; + private ModernTitleBar _titleBar; private ModernDocumentTabStrip _documentTabStrip; private Panel _documentHost; private Panel _statusFooter; @@ -38,6 +39,9 @@ namespace Электронная_Фармация Font = new Font("Segoe UI", 12f); AutoScaleMode = AutoScaleMode.Dpi; KeyPreview = true; + FormBorderStyle = FormBorderStyle.None; + MaximizeBox = true; + MinimizeBox = true; MinimumSize = new Size(900, 620); bindControl.Visible = false; @@ -47,17 +51,21 @@ namespace Электронная_Фармация AppServices.Initialize(_toastService); _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.SetItems(new[] { - new SidebarItem { Key = "price", Text = "ПРАЙС-ЛИСТ", IconGlyph = "\uE8FD" }, - new SidebarItem { Key = "orders", Text = "ЗАКАЗЫ", IconGlyph = "\uE719" }, - new SidebarItem { Key = "invoices", Text = "НАКЛАДНЫЕ", IconGlyph = "\uE8A5" }, - new SidebarItem { Key = "upload", Text = "ЗАГРУЗИТЬ", IconGlyph = "\uE896" }, - new SidebarItem { Key = "send", Text = "ОТПРАВИТЬ", IconGlyph = "\uE725" }, - new SidebarItem { Key = "refusals", Text = "ОТКАЗЫ", IconGlyph = "\uE711" }, - new SidebarItem { Key = "directory", Text = "СПРАВОЧНИК", IconGlyph = "\uE8FD" }, - new SidebarItem { Key = "auth", Text = "АВТОРИЗАЦИЯ", IconGlyph = "\uE192" } + new SidebarItem { Key = "price", Text = "Прайс-лист", IconGlyph = "\uE14C" }, + new SidebarItem { Key = "orders", Text = "Заказы", IconGlyph = "\uE7BF" }, + new SidebarItem { Key = "invoices", Text = "Накладные", IconGlyph = "\uE8A5" }, + new SidebarItem { Key = "upload", Text = "Загрузить", IconGlyph = "\uE896" }, + new SidebarItem { Key = "send", Text = "Отправить", IconGlyph = "\uE725" }, + 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 }; @@ -67,16 +75,22 @@ namespace Электронная_Фармация { Name = "statusFooter", Dock = DockStyle.Bottom, - Height = 28, + Height = 32, Tag = "status-footer", - Padding = new Padding(10, 0, 4, 0) + Padding = new Padding(12, 0, 8, 0) }; _statusFooter.Paint += (_, e) => { - var border = ThemeManager.Colors.Border; - using (var pen = new Pen(border)) + var theme = ThemeManager.Colors; + 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); } }; @@ -118,6 +132,7 @@ namespace Электронная_Фармация Controls.Add(_statusFooter); Controls.Add(_documentTabStrip); Controls.Add(_appHeader); + Controls.Add(_titleBar); _directoryMenu = new ContextMenuStrip(); var contractorsItem = new ToolStripMenuItem("Поставщики"); @@ -167,12 +182,24 @@ namespace Электронная_Фармация { UiThemeHelper.ApplyToControlTree(this); UpdateThemeToggleState(); + _titleBar?.ApplyTheme(ThemeManager.Colors); 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() { if (_documentTabStrip != null) @@ -193,14 +220,49 @@ namespace Электронная_Фармация { ThemeManager.CurrentVariant = Settings.Default.UseDarkTheme ? ThemeVariant.DarkProductivity - : ThemeVariant.PharmaTrust; + : ThemeVariant.ClinicalPro; _themeService.Initialize(this); UpdateThemeToggleState(); UiThemeHelper.ApplyToControlTree(this); + _titleBar?.ApplyTheme(ThemeManager.Colors); WindowChromeHelper.ApplyTitleBarTheme(this, ThemeManager.Colors); + try + { + Icon = CreateAppIcon(); + } + catch + { + // keep default + } 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() { if (_lblCurrentUser == null) diff --git a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs index 2e8d278..5e2282b 100644 --- a/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs +++ b/src/ElectronicPharmacy/Forms/FCheckDatabaseIntegrary.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; @@ -509,76 +509,17 @@ FROM [TempOrderItems];"; } } - // Перед seed-вставкой грузополучателей нужно гарантировать наличие LocationId, - // иначе INSERT в Consignees (LocationId) упадёт на старых версиях БД. + // LocationId нужен для связи с BuyerLocation на сервере. TryAddColumn(conForCheckTables, "Consignees", "LocationId", "nvarchar(64)"); - // Seed-тестовые грузополучатели (аптеки) для удобства ручного тестирования. - // Вставляем только если таблица пустая. - using (var countConsigneesCmd = new SQLiteCommand( - @"SELECT COUNT(*) FROM Consignees;", + // Удаляем старые тестовые грузополучатели — точки создаются в веб-кабинете. + using (var delTest = new SQLiteCommand( + @"DELETE FROM Consignees + WHERE ConsigneesName LIKE 'Тестовая аптека%' + OR ConsigneesAddress LIKE 'Тестовый адрес%';", conForCheckTables)) { - var countObj = countConsigneesCmd.ExecuteScalar(); - var count = 0; - try { count = Convert.ToInt32(countObj); } catch { count = 0; } - - if (count == 0) - { - var testLocationIds = new[] - { - "87368f6d-488b-41a9-b170-edc629522844", - "c4cd845f-820d-45ac-a473-3df1a0b96feb", - "d6187013-00f5-4c1b-b258-c670cf71d115" - }; - - // Тестовые имена/адреса — можно менять под вашу тестовую среду. - var testNames = new[] - { - "Тестовая аптека 1", - "Тестовая аптека 2", - "Тестовая аптека 3" - }; - - var testAddresses = new[] - { - "Тестовый адрес 1", - "Тестовый адрес 2", - "Тестовый адрес 3" - }; - - using (var tx = conForCheckTables.BeginTransaction()) - { - int nextCode; - using (var nextCodeCmd = new SQLiteCommand( - @"SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees;", - conForCheckTables, - tx)) - { - nextCode = Convert.ToInt32(nextCodeCmd.ExecuteScalar()); - } - - for (var i = 0; i < 3; i++) - { - using (var insertCmd = new SQLiteCommand( - @"INSERT INTO Consignees - (codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId) - VALUES (@code, @name, @address, @isDefault, @loc);", - conForCheckTables, - tx)) - { - insertCmd.Parameters.AddWithValue("@code", nextCode + i); - insertCmd.Parameters.AddWithValue("@name", testNames[i]); - insertCmd.Parameters.AddWithValue("@address", testAddresses[i]); - insertCmd.Parameters.AddWithValue("@isDefault", i == 0 ? 1 : 0); - insertCmd.Parameters.AddWithValue("@loc", testLocationIds[i]); - insertCmd.ExecuteNonQuery(); - } - } - - tx.Commit(); - } - } + delTest.ExecuteNonQuery(); } TryAddColumn(conForCheckTables, "Orders", "ConsigneeName", "nvarchar(128)"); @@ -600,7 +541,7 @@ FROM [TempOrderItems];"; { try { - var logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); + var logDir = AppConfig.LogsDirectory; Directory.CreateDirectory(logDir); var logPath = Path.Combine( logDir, diff --git a/src/ElectronicPharmacy/Forms/FConsignees.cs b/src/ElectronicPharmacy/Forms/FConsignees.cs index 274064e..0b433c2 100644 --- a/src/ElectronicPharmacy/Forms/FConsignees.cs +++ b/src/ElectronicPharmacy/Forms/FConsignees.cs @@ -1,8 +1,10 @@ using System; using System.Data; using System.Data.SQLite; +using System.Threading.Tasks; using System.Windows.Forms; using Электронная_Фармация.Classes; +using Электронная_Фармация.Properties; namespace Электронная_Фармация.Forms { @@ -15,10 +17,12 @@ namespace Электронная_Фармация.Forms InitializeComponent(); } - private void FConsignees_Load(object sender, EventArgs e) + private async void FConsignees_Load(object sender, EventArgs e) { UiThemeHelper.ApplyToControlTree(this); + btnAdd.Text = "С сервера"; showMeContentFromConsigneesTable(); + await SyncFromServerAsync(silent: true); } void showMeContentFromConsigneesTable() @@ -49,7 +53,8 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]"; foreach (DataGridViewColumn column in dgvConsignees.Columns) { - column.ReadOnly = column.Name != "Location ID" && column.Name != "Адрес"; + // Редактируем только адрес локально; Location ID и имя приходят с сервера. + column.ReadOnly = column.Name != "Адрес"; } } catch (Exception ex) @@ -88,8 +93,6 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]"; dgvConsignees.EndEdit(); - // Кто должен стать активной аптекой после сохранения. - // По вашему ТЗ: "аптека выбранная в этом окне" => текущая выделенная строка. var selectedName = dgvConsignees.CurrentRow?.Cells["Наименование"]?.Value?.ToString()?.Trim() ?? string.Empty; SelectedConsigneeNameAfterSave = selectedName; @@ -108,14 +111,12 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]"; using (var cmd = new SQLiteCommand( @"UPDATE Consignees - SET ConsigneesAddress = @address, - LocationId = @locationId + SET ConsigneesAddress = @address WHERE idConsignees = @id", connection, tx)) { cmd.Parameters.AddWithValue("@address", row["Адрес"]?.ToString() ?? string.Empty); - cmd.Parameters.AddWithValue("@locationId", row["Location ID"]?.ToString()?.Trim() ?? string.Empty); cmd.Parameters.AddWithValue("@id", Convert.ToInt64(row["Id"])); cmd.ExecuteNonQuery(); } @@ -137,107 +138,58 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]"; Close(); } - private void btnAdd_Click(object sender, EventArgs e) + private async void btnAdd_Click(object sender, EventArgs e) { - using (var dialog = new Form()) + await SyncFromServerAsync(silent: false); + } + + private async Task SyncFromServerAsync(bool silent) + { + try { - dialog.Text = "Новая аптека"; - dialog.FormBorderStyle = FormBorderStyle.FixedDialog; - dialog.StartPosition = FormStartPosition.CenterParent; - // Немного увеличиваем высоту/ширину на случай DPI/шрифтов. - dialog.ClientSize = new System.Drawing.Size(480, 310); - dialog.MaximizeBox = false; - dialog.MinimizeBox = false; - dialog.ShowInTaskbar = false; - - var lblName = new Label { Text = "Наименование", Left = 16, Top = 16, AutoSize = true }; - var txtName = new TextBox { Left = 16, Top = 42, Width = 440, Height = 28 }; - var lblAddress = new Label { Text = "Адрес", Left = 16, Top = 82, AutoSize = true }; - var txtAddress = new TextBox { Left = 16, Top = 108, Width = 440, Height = 28 }; - var lblLocation = new Label { Text = "Location ID", Left = 16, Top = 148, AutoSize = true }; - var txtLocation = new TextBox { Left = 16, Top = 174, Width = 440, Height = 28 }; - - var btnOk = new Elfisa.UI.Controls.ModernButton + btnAdd.Enabled = false; + var client = new ApiClient(); + if (!client.IsAuthenticated) { - Text = "Добавить", - DialogResult = DialogResult.OK - }; - var btnCancel = new Elfisa.UI.Controls.ModernButton - { - Text = "Отмена", - DialogResult = DialogResult.Cancel - }; - - dialog.Controls.AddRange(new Control[] - { - lblName, txtName, lblAddress, txtAddress, lblLocation, txtLocation, btnOk, btnCancel - }); - dialog.AcceptButton = btnOk; - dialog.CancelButton = btnCancel; - UiThemeHelper.ApplyToControlTree(dialog); - - btnOk.Padding = new Padding(12, 4, 12, 4); - btnCancel.Padding = new Padding(12, 4, 12, 4); - Elfisa.UI.Controls.ModernButtonStyles.FitToText(btnOk, 36); - Elfisa.UI.Controls.ModernButtonStyles.FitToText(btnCancel, 36); - btnOk.Height = 36; - btnCancel.Height = 36; - - const int padding = 16; - const int gap = 10; - - // Минимально гарантируем ширину, чтобы обе кнопки поместились. - var requiredWidth = btnOk.Width + btnCancel.Width + gap + padding * 2; - if (dialog.ClientSize.Width < requiredWidth) - { - dialog.ClientSize = new System.Drawing.Size(requiredWidth, dialog.ClientSize.Height); - } - - // Всегда зажимаем в границы формы после вычисления ширины ModernButton'ов. - btnCancel.Left = Math.Max(padding, dialog.ClientSize.Width - btnCancel.Width - padding); - btnCancel.Top = dialog.ClientSize.Height - btnCancel.Height - padding; - - btnOk.Left = Math.Max(padding, btnCancel.Left - btnOk.Width - gap); - btnOk.Top = btnCancel.Top; - - if (dialog.ShowDialog(this) != DialogResult.OK) - { - return; - } - - var name = (txtName.Text ?? string.Empty).Trim(); - var address = (txtAddress.Text ?? string.Empty).Trim(); - var locationId = (txtLocation.Text ?? string.Empty).Trim(); - if (string.IsNullOrWhiteSpace(name)) - { - UiDialogs.ShowError("Укажите наименование аптеки.", "Грузополучатели", this); - return; - } - - using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString)) - { - connection.Open(); - int nextCode = 1; - using (var codeCmd = new SQLiteCommand("SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees", connection)) + var token = Settings.Default.stringToken; + if (string.IsNullOrWhiteSpace(token)) { - nextCode = Convert.ToInt32(codeCmd.ExecuteScalar()); - } - - using (var insert = new SQLiteCommand( - @"INSERT INTO Consignees (codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId) - VALUES (@code, @name, @address, 0, @locationId)", - connection)) - { - insert.Parameters.AddWithValue("@code", nextCode); - insert.Parameters.AddWithValue("@name", name); - insert.Parameters.AddWithValue("@address", address); - insert.Parameters.AddWithValue("@locationId", locationId); - insert.ExecuteNonQuery(); + if (!silent) + { + UiDialogs.ShowInfo( + "Сначала войдите на сервер — грузополучатели подтягиваются из веб-кабинета покупателя.", + "Грузополучатели", + this); + } + return; } + client.SetToken(token); } - ToastNotification.ShowSuccess("Аптека добавлена"); + 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; } } @@ -249,15 +201,15 @@ order by [ConsigneesUseAsDefault] desc, [ConsigneesName]"; return; } - var name = dgvConsignees.CurrentRow.Cells["Наименование"]?.Value?.ToString(); + var name = dgvConsignees.CurrentRow.Cells["Наименование"]?.Value?.ToString()?.Trim(); if (string.IsNullOrWhiteSpace(name)) { return; } ConsigneeHelper.SetActiveConsignee(name); - ToastNotification.ShowSuccess($"Аптека по умолчанию: {name}"); showMeContentFromConsigneesTable(); + ToastNotification.ShowSuccess($"Аптека по умолчанию: {name}"); } } } diff --git a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs index 969822d..0bc77ec 100644 --- a/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs +++ b/src/ElectronicPharmacy/HelpForms/HF_DownloadDataFromServer.cs @@ -54,6 +54,21 @@ namespace Электронная_Фармация.HelpForms SetOverallProgress(1, "Авторизация..."); await EnsureAuthenticatedAsync(client); + try + { + AppendStatus("Загружаю грузополучателей с сервера..."); + var locations = await client.GetBuyerLocationsAsync(); + var synced = ConsigneeHelper.SyncFromServer(locations); + AppendStatus(synced > 0 + ? $"Грузополучателей обновлено: {synced}" + : "Грузополучателей на сервере пока нет"); + } + catch (Exception locEx) + { + AppendStatus($"Грузополучатели не обновлены: {locEx.Message}"); + AppDebugLog.Error("Download", "Не удалось синхронизировать грузополучателей", locEx); + } + SetOverallProgress(3, "Загрузка прайса с сервера..."); AppendStatus("Загружаю сводный прайс..."); var allSuppliersSummary = await GetFullPriceSummaryPagedAsync(client); diff --git a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs index 1fa4f87..1090112 100644 --- a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs +++ b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs @@ -1,36 +1,37 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// ����� �������� �� ���� ������ ��������������� ��������� ������� -// ������ ���������. �������� �������� ���� ��������� ��� ��������� ��������, -// ��������� �� �������. -[assembly: AssemblyTitle("����������� ��������")] +// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ +// пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ. пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ, +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ. +[assembly: AssemblyTitle("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("����������� ��������")] -[assembly: AssemblyCopyright("Copyright � 2023")] +[assembly: AssemblyProduct("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")] +[assembly: AssemblyCopyright("Copyright пїЅ 2023")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -// ��������� �������� False ��� ��������� ComVisible ������ ���� � ���� ������ ���������� -// ��� ����������� COM. ���� ���������� ���������� � ���� � ���� ������ ����� -// COM, ������� ���������� ������� ComVisible � TRUE ��� ����� ����. +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ False пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ +// пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ COM. пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ +// COM, пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅ TRUE пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ. [assembly: ComVisible(false)] -// ��������� GUID ������ ��� ������������� ���������� �����, ���� ���� ������ ����� ������� ��� COM +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ GUID пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ, пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ COM [assembly: Guid("145d8060-4187-4871-b4fb-8df795727b66")] -// �������� � ������ ������ ������� �� ��������� ���� ������� ��������: +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ: // -// �������� ����� ������ -// �������������� ����� ������ -// ����� ������ -// �������� +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ +// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ // -// ����� ������ ��� �������� ��� ������� ������ ������ � �������� �� ��������� -// ��������� "*", ��� �������� ����: +// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ +// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.20.0")] -[assembly: AssemblyFileVersion("1.0.20.0")] +[assembly: AssemblyVersion("1.0.21.0")] +[assembly: AssemblyFileVersion("1.0.21.0")] + diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs index 025f07b..fc0a3c1 100644 --- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs +++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs @@ -131,6 +131,18 @@ 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("")] diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings index 51cd9d0..5d2e78b 100644 --- a/src/ElectronicPharmacy/Properties/Settings.settings +++ b/src/ElectronicPharmacy/Properties/Settings.settings @@ -29,6 +29,9 @@ False + + 0 + diff --git a/src/ElectronicPharmacy/Resources/elfisa.ico b/src/ElectronicPharmacy/Resources/elfisa.ico new file mode 100644 index 0000000000000000000000000000000000000000..20dd21e396fe6f40e0d1f8abe5fa700d6f8032a6 GIT binary patch literal 7693 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 bnf1Txa=qd50e7eW_Tc6IIoALE|Iz;d?4;@$ literal 0 HcmV?d00001 diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs index 8a56067..a076a83 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.Designer.cs @@ -1,4 +1,4 @@ -namespace Электронная_Фармация.UserControls +namespace Электронная_Фармация.UserControls { partial class UCPriceList { @@ -99,7 +99,7 @@ this.SplitPriceList.Panel2.Controls.Add(this.POrderInfo); this.SplitPriceList.Size = new System.Drawing.Size(1053, 532); this.SplitPriceList.SplitterDistance = 290; - this.SplitPriceList.SplitterWidth = 2; + this.SplitPriceList.SplitterWidth = 6; this.SplitPriceList.TabIndex = 0; // // TLPPriceList diff --git a/src/ElectronicPharmacy/UserControls/UCPriceList.cs b/src/ElectronicPharmacy/UserControls/UCPriceList.cs index 6bad437..5f63e79 100644 --- a/src/ElectronicPharmacy/UserControls/UCPriceList.cs +++ b/src/ElectronicPharmacy/UserControls/UCPriceList.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; @@ -78,13 +78,14 @@ namespace Электронная_Фармация.UserControls private void UCPriceList_Load(object sender, EventArgs e) { EnsureBrowseSplit(); + EnsureChromeLayout(); UiThemeHelper.ApplyToControlTree(this); Tag = "chrome-child"; AlignSearchRow(); + LayoutContentBelowSearch(); - panel1.SizeChanged += (_, __) => LayoutMarkupRow(); - LayoutMarkupRow(); + SizeChanged += (_, __) => LayoutContentBelowSearch(); txtConsignee.Enabled = false; txtConsignee.Text = _WorkWithConsignee; @@ -97,6 +98,120 @@ namespace Электронная_Фармация.UserControls txtSearchGoodNameInPriceList.Focus(); } + /// + /// Поиск остаётся сверху как в оригинале; контент явно ниже — без Dock-наложения. + /// + private void EnsureChromeLayout() + { + // Убираем старую dock-панель, если осталась от прошлых сборок в той же сессии. + var oldToolbar = Controls["priceToolbar"]; + if (oldToolbar != null) + { + var toRestore = oldToolbar.Controls.Cast().ToArray(); + foreach (var control in toRestore) + { + oldToolbar.Controls.Remove(control); + Controls.Add(control); + control.BringToFront(); + } + + Controls.Remove(oldToolbar); + oldToolbar.Dispose(); + } + + // Строка наценки скрыта — проценты берутся из настроек/дефолта без UI-прослойки. + if (TLPPriceList.RowStyles.Count >= 2) + { + TLPPriceList.RowStyles[1].SizeType = SizeType.Absolute; + TLPPriceList.RowStyles[1].Height = 0; + } + + panel1.Visible = false; + panel1.Height = 0; + + SplitPriceList.Panel1MinSize = 120; + SplitPriceList.Panel2MinSize = 80; + SplitPriceList.IsSplitterFixed = false; + SplitPriceList.FixedPanel = FixedPanel.None; + SplitPriceList.SplitterWidth = 6; + + // Восстановить положение сплиттера корзины, если пользователь уже двигал. + RestoreCartSplitterDistance(); + SplitPriceList.SplitterMoved += (_, __) => PersistCartSplitterDistance(); + + dgvPriceList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None; + dgvPriceList.ColumnHeadersVisible = true; + dgvPriceList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; + if (dgvPriceList.ColumnHeadersHeight < 34) + { + dgvPriceList.ColumnHeadersHeight = 36; + } + + if (!POrderInfo.Controls.ContainsKey("cartHeader")) + { + var cartHeader = new ModernSectionHeader + { + Name = "cartHeader", + Height = 32, + HeaderText = "Корзина" + }; + cartHeader.ApplyTheme(Elfisa.UI.Theming.ThemeManager.Colors, cartStyle: true); + cartHeader.Dock = DockStyle.Top; + POrderInfo.Controls.Add(cartHeader); + tabOrderItems.SendToBack(); + cartHeader.BringToFront(); + } + } + + private void LayoutContentBelowSearch() + { + const int searchBand = 52; + PContentPriceList.Dock = DockStyle.None; + PContentPriceList.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; + PContentPriceList.SetBounds( + 0, + searchBand, + Math.Max(100, ClientSize.Width), + Math.Max(120, ClientSize.Height - searchBand)); + } + + private void RestoreCartSplitterDistance() + { + try + { + var saved = Properties.Settings.Default.PriceListSplitterDistance; + if (saved <= 0 || SplitPriceList.Height < 200) + { + return; + } + + var max = Math.Max( + SplitPriceList.Panel1MinSize, + SplitPriceList.Height - SplitPriceList.Panel2MinSize - SplitPriceList.SplitterWidth); + SplitPriceList.SplitterDistance = Math.Max(SplitPriceList.Panel1MinSize, Math.Min(saved, max)); + } + catch + { + // ignore until layout ready + } + } + + private void PersistCartSplitterDistance() + { + try + { + if (SplitPriceList.SplitterDistance > 80) + { + Properties.Settings.Default.PriceListSplitterDistance = SplitPriceList.SplitterDistance; + Properties.Settings.Default.Save(); + } + } + catch + { + // ignore + } + } + private void EnsureBrowseSplit() { if (splitNameMgBrowse != null) @@ -112,13 +227,35 @@ namespace Электронная_Фармация.UserControls Name = "splitNameMgBrowse", Dock = DockStyle.Fill, Orientation = Orientation.Vertical, - SplitterWidth = 4, - Visible = false + SplitterWidth = 6, + Visible = false, + Panel1MinSize = 100, + Panel2MinSize = 80, + IsSplitterFixed = false, + FixedPanel = FixedPanel.None }; splitNameMgBrowse.Panel1.Controls.Add(dgvBrowseNames); splitNameMgBrowse.Panel2.Controls.Add(dgvBrowseMg); - TLPPriceList.Controls.Add(splitNameMgBrowse, 0, 0); + var gridHost = TLPPriceList.Controls["priceGridHost"] as Panel; + if (gridHost == null) + { + gridHost = new Panel + { + Name = "priceGridHost", + Dock = DockStyle.Fill, + Margin = new Padding(0), + Padding = new Padding(0), + Tag = "chrome-child" + }; + + TLPPriceList.Controls.Remove(dgvPriceList); + dgvPriceList.Dock = DockStyle.Fill; + gridHost.Controls.Add(dgvPriceList); + TLPPriceList.Controls.Add(gridHost, 0, 0); + } + + gridHost.Controls.Add(splitNameMgBrowse); splitNameMgBrowse.BringToFront(); dgvBrowseNames.SelectionChanged += dgvBrowseNames_SelectionChanged; @@ -157,11 +294,13 @@ namespace Электронная_Фармация.UserControls private void AlignSearchRow() { - const int searchY = 9; + const int searchY = 10; const int searchHeight = 32; - const int clearGap = 2; + const int clearGap = 4; + const int leftPad = 10; - txtSearchGoodNameInPriceList.SetBounds(78, searchY, 313, searchHeight); + label2.Location = new Point(leftPad, searchY + 4); + txtSearchGoodNameInPriceList.SetBounds(label2.Right + 6, searchY, 300, searchHeight); btnClearSearchInPriceList.SetBounds( txtSearchGoodNameInPriceList.Right + clearGap, searchY, @@ -169,38 +308,23 @@ namespace Электронная_Фармация.UserControls searchHeight); ModernButtonStyles.ApplyClear(btnClearSearchInPriceList); + lblSupplierFilter.Location = new Point(btnClearSearchInPriceList.Right + 16, searchY + 4); + comboSupplierFilter.SetBounds(lblSupplierFilter.Right + 6, searchY, 240, searchHeight); + btnClearSupplierFilter.SetBounds(comboSupplierFilter.Right + clearGap, searchY, 32, searchHeight); btnClearSupplierFilter.Height = searchHeight; + + // Поиск всегда поверх контента, но контент начинается ниже searchBand. + label2.BringToFront(); + txtSearchGoodNameInPriceList.BringToFront(); + btnClearSearchInPriceList.BringToFront(); + lblSupplierFilter.BringToFront(); + comboSupplierFilter.BringToFront(); + btnClearSupplierFilter.BringToFront(); } private void LayoutMarkupRow() { - // Keep markup row readable at different DPI/window sizes. - try - { - var paddingLeft = 8; - var y = 30; - var gap = 8; - - label4.Location = new Point(paddingLeft, y + 2); - - numericPercent.Location = new Point(label4.Right + 6, y); - numericPercent.Width = 56; - - checkUsePercentageAsDefault.AutoSize = true; - checkUsePercentageAsDefault.Location = new Point( - Math.Max(numericPercent.Right + gap, panel1.ClientSize.Width - checkUsePercentageAsDefault.PreferredSize.Width - 10), - y + 2); - - lblPriceForGood.AutoSize = false; - lblPriceForGood.AutoEllipsis = true; - lblPriceForGood.Location = new Point(numericPercent.Right + gap, y + 2); - lblPriceForGood.Height = 22; - lblPriceForGood.Width = Math.Max(90, checkUsePercentageAsDefault.Left - lblPriceForGood.Left - gap); - } - catch - { - // best-effort layout; ignore - } + // UI наценки скрыт; метод оставлен на случай повторного включения. } private void InitializeSupplierFilter() @@ -725,10 +849,21 @@ namespace Электронная_Фармация.UserControls { EnsureBrowseSplit(); + var gridHost = TLPPriceList.Controls["priceGridHost"] as Panel; + if (gridHost != null) + { + gridHost.Dock = DockStyle.Fill; + if (gridHost.Padding.Top < 8) + { + gridHost.Padding = new Padding(0, 8, 0, 0); + } + } + if (browseVisible) { dgvPriceList.Visible = false; splitNameMgBrowse.Visible = true; + splitNameMgBrowse.Dock = DockStyle.Fill; splitNameMgBrowse.BringToFront(); var width = Math.Max(200, splitNameMgBrowse.Width); @@ -754,6 +889,9 @@ namespace Электронная_Фармация.UserControls splitNameMgBrowse.Visible = false; dgvPriceList.Visible = true; + dgvPriceList.Dock = DockStyle.Fill; + dgvPriceList.ColumnHeadersVisible = true; + dgvPriceList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None; dgvPriceList.BringToFront(); } } diff --git a/src/Elfisa.UI/Controls/ModernAppHeader.cs b/src/Elfisa.UI/Controls/ModernAppHeader.cs index 79482cd..2b43ca1 100644 --- a/src/Elfisa.UI/Controls/ModernAppHeader.cs +++ b/src/Elfisa.UI/Controls/ModernAppHeader.cs @@ -9,7 +9,7 @@ using Elfisa.UI.Theming; namespace Elfisa.UI.Controls { /// - /// Верхняя синяя шапка как в оригинале: 3 равные кнопки, owner-draw + плавный hover (Fluent). + /// Верхняя навигация: бренд + пункты (иконка сверху, подпись снизу). /// public class ModernAppHeader : Panel { @@ -21,11 +21,13 @@ namespace Elfisa.UI.Controls private string _hoverKey; private float _hoverProgress; + private const int BrandWidth = 118; + public event EventHandler ItemSelected; public ModernAppHeader() { - Height = 78; + Height = 64; Dock = DockStyle.Top; Cursor = Cursors.Hand; SetStyle(ControlStyles.AllPaintingInWmPaint | @@ -64,21 +66,16 @@ namespace Elfisa.UI.Controls var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; var rect = ClientRectangle; - using (var brush = new LinearGradientBrush(rect, theme.TitleBarTop, theme.TitleBarBottom, 90f)) + // Сплошной основной teal — без градиента, в тон AccentPrimary. + using (var fill = new SolidBrush(theme.AccentPrimary)) { - g.FillRectangle(brush, rect); + g.FillRectangle(fill, rect); } - using (var sheen = new LinearGradientBrush( - new Rectangle(0, 0, Width, Height / 2), - Color.FromArgb(28, 255, 255, 255), - Color.FromArgb(0, 255, 255, 255), - LinearGradientMode.Vertical)) - { - g.FillRectangle(sheen, 0, 0, Width, Height / 2); - } + DrawBrand(g, theme); if (_items.Count == 0) { @@ -86,72 +83,125 @@ namespace Elfisa.UI.Controls } _hitAreas.Clear(); - var gap = 8; - var btnWidth = (Width - gap * (_items.Count + 1)) / _items.Count; - var btnHeight = Height - 12; - var top = 6; + const int gap = 6; + const int padRight = 8; + var btnHeight = Height - 10; + var top = 5; + var x = BrandWidth + 6; - using (var iconFont = new Font("Segoe MDL2 Assets", 18f)) - using (var textFont = new Font("Segoe UI", 10f, FontStyle.Bold)) + using (var iconFont = FluentFonts.CreateIconFont(18f)) + using (var textFont = new Font("Segoe UI Semibold", 8.5f, FontStyle.Bold)) { + var iconFormat = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center, + FormatFlags = StringFormatFlags.NoWrap + }; + + // Равная ширина слотов — подписи целиком помещаются на широком окне. + var available = Math.Max(100, Width - BrandWidth - padRight - 6); + var btnWidth = Math.Max(72, (available - gap * (_items.Count - 1)) / _items.Count); + for (var i = 0; i < _items.Count; i++) { var item = _items[i]; - var btnRect = new Rectangle(gap + i * (btnWidth + gap), top, btnWidth, btnHeight); + var btnRect = new Rectangle(x, top, btnWidth, btnHeight); _hitAreas[item.Key] = btnRect; + x += btnWidth + gap; var isActive = item.Key == _activeKey; var isHover = item.Key == _hoverKey; - var hoverBoost = isHover ? _hoverProgress * 0.22f : 0f; - var baseAlpha = isActive ? 0.28f : 0.08f; - var alpha = (int)(255 * Math.Min(0.55f, baseAlpha + hoverBoost)); - var fill = Color.FromArgb(alpha, 255, 255, 255); + var hoverBoost = isHover ? _hoverProgress * 0.18f : 0f; + var baseAlpha = isActive ? 0.34f : 0.07f; + var alpha = (int)(255 * Math.Min(0.52f, baseAlpha + hoverBoost)); - using (var fillBrush = new SolidBrush(fill)) + using (var fillBrush = new SolidBrush(Color.FromArgb(alpha, 255, 255, 255))) { GraphicsExtensions.FillRoundedRectangle(g, fillBrush, btnRect, theme.CornerRadiusSmall); } if (isActive) { - using (var pen = new Pen(Color.FromArgb(180, 255, 255, 255), 1f)) + using (var pen = new Pen(Color.FromArgb(210, 255, 255, 255), 1.1f)) { GraphicsExtensions.DrawRoundedRectangle(g, pen, btnRect, theme.CornerRadiusSmall); } + + using (var accent = new SolidBrush(Color.FromArgb(235, 255, 255, 255))) + { + g.FillRectangle(accent, btnRect.X + 10, btnRect.Bottom - 3, btnRect.Width - 20, 2); + } } else if (isHover && _hoverProgress > 0.05f) { - using (var pen = new Pen(Color.FromArgb((int)(120 * _hoverProgress), 255, 255, 255), 1f)) + using (var pen = new Pen(Color.FromArgb((int)(110 * _hoverProgress), 255, 255, 255), 1f)) { GraphicsExtensions.DrawRoundedRectangle(g, pen, btnRect, theme.CornerRadiusSmall); } } - var iconRect = new Rectangle(btnRect.X, btnRect.Y + 8, btnRect.Width, 26); - var textRect = new Rectangle(btnRect.X, btnRect.Y + 34, btnRect.Width, btnRect.Height - 34); - if (!string.IsNullOrEmpty(item.IconGlyph)) { - TextRenderer.DrawText( - g, - item.IconGlyph, - iconFont, - iconRect, - Color.White, - TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine); + var iconRect = new RectangleF(btnRect.X, btnRect.Y + 4, btnRect.Width, 26); + using (var brush = new SolidBrush(Color.White)) + { + g.DrawString(item.IconGlyph, iconFont, brush, iconRect, iconFormat); + } } + var textRect = new Rectangle(btnRect.X + 2, btnRect.Y + 30, btnRect.Width - 4, btnRect.Height - 34); TextRenderer.DrawText( g, item.Text, textFont, textRect, Color.White, - TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine); + TextFormatFlags.HorizontalCenter | TextFormatFlags.Top | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine); } } } + private void DrawBrand(Graphics g, ThemeColors theme) + { + var brandRect = new Rectangle(0, 0, BrandWidth, Height); + using (var dim = new SolidBrush(Color.FromArgb(28, 0, 0, 0))) + { + g.FillRectangle(dim, brandRect); + } + + using (var pen = new Pen(Color.FromArgb(50, 255, 255, 255), 1f)) + { + g.DrawLine(pen, BrandWidth - 1, 8, BrandWidth - 1, Height - 8); + } + + var iconSize = 22; + var iconRect = new Rectangle(12, (Height - iconSize) / 2, iconSize, iconSize); + using (var iconBrush = new SolidBrush(Color.FromArgb(40, 255, 255, 255))) + { + GraphicsExtensions.FillRoundedRectangle(g, iconBrush, iconRect, 6); + } + + using (var crossPen = new Pen(Color.White, 2f) { StartCap = LineCap.Round, EndCap = LineCap.Round }) + { + var cx = iconRect.X + iconRect.Width / 2f; + var cy = iconRect.Y + iconRect.Height / 2f; + g.DrawLine(crossPen, cx - 5, cy, cx + 5, cy); + g.DrawLine(crossPen, cx, cy - 5, cx, cy + 5); + } + + using (var brandFont = new Font("Segoe UI Semibold", 11f, FontStyle.Bold)) + { + TextRenderer.DrawText( + g, + "ЭльФиСА", + brandFont, + new Rectangle(40, 0, BrandWidth - 46, Height), + Color.White, + TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.SingleLine); + } + } + protected override void OnPaintBackground(PaintEventArgs e) { } diff --git a/src/Elfisa.UI/Controls/ModernButton.cs b/src/Elfisa.UI/Controls/ModernButton.cs index 520a6fc..1b1a8ce 100644 --- a/src/Elfisa.UI/Controls/ModernButton.cs +++ b/src/Elfisa.UI/Controls/ModernButton.cs @@ -445,13 +445,15 @@ namespace Elfisa.UI.Controls else { var borderColor = _buttonStyle == ModernButtonStyle.DangerSoft - ? GraphicsExtensions.Blend(theme.Danger, Color.White, 0.55f) + ? GraphicsExtensions.Blend(theme.Danger, Color.White, 0.45f) : _buttonStyle == ModernButtonStyle.Subtle ? theme.Border : theme.InputBorder; if (_hover && Enabled) { - borderColor = theme.InputBorderFocus; + borderColor = _buttonStyle == ModernButtonStyle.DangerSoft + ? theme.Danger + : theme.InputBorderFocus; } using (var pen = new Pen(borderColor, 1f)) diff --git a/src/Elfisa.UI/Controls/ModernButtonStyles.cs b/src/Elfisa.UI/Controls/ModernButtonStyles.cs index 712e569..1a6dc19 100644 --- a/src/Elfisa.UI/Controls/ModernButtonStyles.cs +++ b/src/Elfisa.UI/Controls/ModernButtonStyles.cs @@ -30,7 +30,8 @@ namespace Elfisa.UI.Controls if (IsDangerAction(text)) { - ApplyAction(button, ModernButtonStyle.Danger); + // Мягкий danger в тон Clinical Pro — без кислотно-красного «крика». + ApplyAction(button, ModernButtonStyle.DangerSoft); return; } @@ -73,7 +74,7 @@ namespace Elfisa.UI.Controls return; } - button.ButtonStyle = ModernButtonStyle.Danger; + button.ButtonStyle = ModernButtonStyle.DangerSoft; button.CompactIconMode = true; button.Image = null; button.BackgroundImage = null; diff --git a/src/Elfisa.UI/Controls/ModernDocumentTabStrip.cs b/src/Elfisa.UI/Controls/ModernDocumentTabStrip.cs index 20ed68a..9aa107d 100644 --- a/src/Elfisa.UI/Controls/ModernDocumentTabStrip.cs +++ b/src/Elfisa.UI/Controls/ModernDocumentTabStrip.cs @@ -177,11 +177,18 @@ namespace Elfisa.UI.Controls g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; - using (var bgBrush = new SolidBrush(theme.BackgroundSecondary)) + // Та же «кожа», что у шапки/футера: лёгкий teal-тинт, не голый slate. + var stripBg = GraphicsExtensions.Blend(theme.Surface, theme.AccentPrimarySoft, 0.55f); + using (var bgBrush = new SolidBrush(stripBg)) { g.FillRectangle(bgBrush, ClientRectangle); } + using (var edge = new Pen(theme.AccentPrimaryBorder, 1f)) + { + g.DrawLine(edge, 0, Height - 1, Width, Height - 1); + } + if (_tabs.Count == 0) { return; @@ -198,8 +205,8 @@ namespace Elfisa.UI.Controls const int toggleReserve = 44; var maxTabRight = Math.Max(10, Width - toggleReserve); + using (var closeFont = FluentFonts.CreateIconFont(10f)) using (var textFont = theme.FontSemibold14 ?? new Font("Segoe UI", 10f, FontStyle.Bold)) - using (var closeFont = new Font("Segoe MDL2 Assets", 10f, FontStyle.Regular)) { foreach (var tab in _tabs) { @@ -218,25 +225,33 @@ namespace Elfisa.UI.Controls var backColor = isActive ? theme.SurfaceElevated - : theme.Surface; + : GraphicsExtensions.Blend(theme.Surface, theme.AccentPrimarySoft, 0.35f); if (!isActive && isHover) { - backColor = GraphicsExtensions.Blend(backColor, theme.AccentPrimarySoft, 0.35f); + backColor = GraphicsExtensions.Blend(backColor, theme.AccentPrimarySoft, 0.65f); } - var foreColor = isActive ? theme.TextPrimary : theme.TextSecondary; - var borderColor = isActive ? theme.AccentPrimaryBorder : theme.BorderSubtle; + var foreColor = isActive ? theme.AccentPrimary : theme.TextMuted; + var borderColor = isActive ? theme.AccentPrimary : theme.AccentPrimaryBorder; using (var brush = new SolidBrush(backColor)) { GraphicsExtensions.FillRoundedRectangle(g, brush, tabRect, theme.CornerRadiusSmall); } - using (var pen = new Pen(borderColor, isActive ? 1.2f : 1f)) + using (var pen = new Pen(borderColor, isActive ? 1.4f : 1f)) { GraphicsExtensions.DrawRoundedRectangle(g, pen, tabRect, theme.CornerRadiusSmall); } + if (isActive) + { + using (var accent = new SolidBrush(theme.AccentPrimary)) + { + g.FillRectangle(accent, tabRect.X + 10, tabRect.Bottom - 3, tabRect.Width - 20, 2); + } + } + var closeRect = new Rectangle(tabRect.Right - closeSize - 8, tabRect.Y + (tabRect.Height - closeSize) / 2, closeSize, closeSize); _closeHitAreas[tab.Id] = closeRect; @@ -291,7 +306,7 @@ namespace Elfisa.UI.Controls } var glyph = _isDarkTheme ? "\uE706" : "\uE708"; - using (var iconFont = new Font("Segoe MDL2 Assets", 11f)) + using (var iconFont = FluentFonts.CreateIconFont(12f)) { TextRenderer.DrawText( g, diff --git a/src/Elfisa.UI/Controls/ModernSplitContainer.cs b/src/Elfisa.UI/Controls/ModernSplitContainer.cs index a6e3dc2..579e295 100644 --- a/src/Elfisa.UI/Controls/ModernSplitContainer.cs +++ b/src/Elfisa.UI/Controls/ModernSplitContainer.cs @@ -10,7 +10,9 @@ namespace Elfisa.UI.Controls { SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true); BorderStyle = BorderStyle.None; - SplitterWidth = 2; + SplitterWidth = 6; + IsSplitterFixed = false; + FixedPanel = FixedPanel.None; } public void ApplyTheme(ThemeColors theme) diff --git a/src/Elfisa.UI/Controls/ModernTitleBar.cs b/src/Elfisa.UI/Controls/ModernTitleBar.cs new file mode 100644 index 0000000..1617e76 --- /dev/null +++ b/src/Elfisa.UI/Controls/ModernTitleBar.cs @@ -0,0 +1,346 @@ +using System; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Elfisa.UI.Theming; + +namespace Elfisa.UI.Controls +{ + /// + /// Системная шапка: заголовок + свернуть/развернуть/закрыть. + /// Иконки рисуются геометрией (не глифы) — как в Win11, без «ломаных» кнопок. + /// + public sealed class ModernTitleBar : Panel + { + private const int ButtonWidth = 40; + private enum ChromeKind { None, Minimize, Maximize, Close } + + private Form _owner; + private ThemeColors _theme; + private ChromeKind _hover = ChromeKind.None; + private ChromeKind _pressed = ChromeKind.None; + private bool _isMaximized; + private readonly ToolTip _tip = new ToolTip { ShowAlways = false, AutoPopDelay = 2000 }; + + public ModernTitleBar() + { + Height = 30; + Dock = DockStyle.Top; + Cursor = Cursors.Default; + DoubleBuffered = true; + SetStyle(ControlStyles.AllPaintingInWmPaint | + ControlStyles.UserPaint | + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | + ControlStyles.Selectable, true); + TabStop = false; + ApplyTheme(ThemeManager.Colors); + } + + public void Attach(Form owner) + { + _owner = owner; + if (_owner == null) + { + return; + } + + _owner.TextChanged += (_, __) => Invalidate(); + _owner.Resize += (_, __) => + { + _isMaximized = _owner.WindowState == FormWindowState.Maximized; + Invalidate(); + }; + _isMaximized = _owner.WindowState == FormWindowState.Maximized; + Invalidate(); + } + + public void ApplyTheme(ThemeColors theme) + { + _theme = theme ?? ThemeManager.Colors; + BackColor = _theme.AccentPrimary; + Invalidate(); + } + + protected override void OnPaintBackground(PaintEventArgs e) + { + } + + protected override void OnPaint(PaintEventArgs e) + { + var theme = _theme ?? ThemeManager.Colors; + var g = e.Graphics; + g.SmoothingMode = SmoothingMode.AntiAlias; + g.PixelOffsetMode = PixelOffsetMode.HighQuality; + + using (var fill = new SolidBrush(theme.AccentPrimary)) + { + g.FillRectangle(fill, ClientRectangle); + } + + DrawTitle(g, theme); + DrawChromeButton(g, ChromeKind.Minimize, theme); + DrawChromeButton(g, ChromeKind.Maximize, theme); + DrawChromeButton(g, ChromeKind.Close, theme); + } + + private void DrawTitle(Graphics g, ThemeColors theme) + { + var title = _owner == null || string.IsNullOrWhiteSpace(_owner.Text) + ? "Электронная Фармация" + : _owner.Text; + var textRect = new Rectangle(12, 0, Math.Max(40, Width - ButtonWidth * 3 - 16), Height); + using (var font = new Font("Segoe UI Semibold", 9f, FontStyle.Bold)) + { + TextRenderer.DrawText( + g, + title, + font, + textRect, + theme.TextOnAccent, + TextFormatFlags.VerticalCenter | TextFormatFlags.Left | TextFormatFlags.EndEllipsis | TextFormatFlags.SingleLine); + } + } + + private Rectangle GetButtonRect(ChromeKind kind) + { + var index = kind == ChromeKind.Close ? 0 : kind == ChromeKind.Maximize ? 1 : 2; + return new Rectangle(Width - ButtonWidth * (index + 1), 0, ButtonWidth, Height); + } + + private void DrawChromeButton(Graphics g, ChromeKind kind, ThemeColors theme) + { + var rect = GetButtonRect(kind); + var isHover = _hover == kind; + var isPressed = _pressed == kind; + + if (isHover || isPressed) + { + Color hoverFill; + if (kind == ChromeKind.Close) + { + hoverFill = isPressed + ? Color.FromArgb(180, 32, 28) + : Color.FromArgb(232, 17, 35); + } + else + { + var alpha = isPressed ? 55 : 38; + hoverFill = Color.FromArgb(alpha, 255, 255, 255); + } + + using (var brush = new SolidBrush(hoverFill)) + { + g.FillRectangle(brush, rect); + } + } + + var iconColor = theme.TextOnAccent; + var cx = rect.X + rect.Width / 2f; + var cy = rect.Y + rect.Height / 2f; + + using (var pen = new Pen(iconColor, 1.15f)) + { + pen.StartCap = LineCap.Round; + pen.EndCap = LineCap.Round; + pen.LineJoin = LineJoin.Round; + + switch (kind) + { + case ChromeKind.Minimize: + g.DrawLine(pen, cx - 5f, cy, cx + 5f, cy); + break; + + case ChromeKind.Maximize: + if (_isMaximized) + { + // restore: два смещённых квадрата + var back = new RectangleF(cx - 3.2f, cy - 5.2f, 8.5f, 8.5f); + var front = new RectangleF(cx - 5.2f, cy - 3.2f, 8.5f, 8.5f); + g.DrawRectangle(pen, back.X, back.Y, back.Width, back.Height); + + // залить передний квадрат цветом шапки (с учётом hover), чтобы «обрезать» задний + var coverColor = theme.AccentPrimary; + if (isHover || isPressed) + { + coverColor = Color.FromArgb( + 255, + BlendChannel(theme.AccentPrimary.R, 255, isPressed ? 0.22f : 0.15f), + BlendChannel(theme.AccentPrimary.G, 255, isPressed ? 0.22f : 0.15f), + BlendChannel(theme.AccentPrimary.B, 255, isPressed ? 0.22f : 0.15f)); + } + + using (var cover = new SolidBrush(coverColor)) + { + g.FillRectangle(cover, Rectangle.Round(RectangleF.Inflate(front, 1f, 1f))); + } + + g.DrawRectangle(pen, front.X, front.Y, front.Width, front.Height); + } + else + { + var box = new RectangleF(cx - 4.5f, cy - 4.5f, 9f, 9f); + g.DrawRectangle(pen, box.X, box.Y, box.Width, box.Height); + } + break; + + case ChromeKind.Close: + g.DrawLine(pen, cx - 4.5f, cy - 4.5f, cx + 4.5f, cy + 4.5f); + g.DrawLine(pen, cx + 4.5f, cy - 4.5f, cx - 4.5f, cy + 4.5f); + break; + } + } + } + + private ChromeKind HitTest(Point p) + { + if (GetButtonRect(ChromeKind.Close).Contains(p)) return ChromeKind.Close; + if (GetButtonRect(ChromeKind.Maximize).Contains(p)) return ChromeKind.Maximize; + if (GetButtonRect(ChromeKind.Minimize).Contains(p)) return ChromeKind.Minimize; + return ChromeKind.None; + } + + protected override void OnMouseMove(MouseEventArgs e) + { + var hit = HitTest(e.Location); + if (hit != _hover) + { + _hover = hit; + Cursor = hit == ChromeKind.None ? Cursors.Default : Cursors.Hand; + UpdateTip(hit); + Invalidate(); + } + + base.OnMouseMove(e); + } + + protected override void OnMouseLeave(EventArgs e) + { + if (_hover != ChromeKind.None || _pressed != ChromeKind.None) + { + _hover = ChromeKind.None; + _pressed = ChromeKind.None; + Cursor = Cursors.Default; + _tip.Hide(this); + Invalidate(); + } + + base.OnMouseLeave(e); + } + + protected override void OnMouseDown(MouseEventArgs e) + { + if (e.Button != MouseButtons.Left) + { + base.OnMouseDown(e); + return; + } + + var hit = HitTest(e.Location); + if (hit != ChromeKind.None) + { + _pressed = hit; + Invalidate(); + base.OnMouseDown(e); + return; + } + + if (e.Clicks >= 2) + { + ToggleMaximize(); + } + else if (_owner != null) + { + ReleaseCapture(); + SendMessage(_owner.Handle, 0xA1, (IntPtr)2, IntPtr.Zero); + } + + base.OnMouseDown(e); + } + + protected override void OnMouseUp(MouseEventArgs e) + { + if (e.Button == MouseButtons.Left && _pressed != ChromeKind.None) + { + var hit = HitTest(e.Location); + var action = _pressed; + _pressed = ChromeKind.None; + Invalidate(); + + if (hit == action) + { + Execute(action); + } + } + + base.OnMouseUp(e); + } + + private void Execute(ChromeKind kind) + { + if (_owner == null) + { + return; + } + + switch (kind) + { + case ChromeKind.Minimize: + _owner.WindowState = FormWindowState.Minimized; + break; + case ChromeKind.Maximize: + ToggleMaximize(); + break; + case ChromeKind.Close: + _owner.Close(); + break; + } + } + + private void ToggleMaximize() + { + if (_owner == null) + { + return; + } + + _owner.WindowState = _owner.WindowState == FormWindowState.Maximized + ? FormWindowState.Normal + : FormWindowState.Maximized; + _isMaximized = _owner.WindowState == FormWindowState.Maximized; + Invalidate(); + } + + private void UpdateTip(ChromeKind kind) + { + string text = null; + switch (kind) + { + case ChromeKind.Minimize: text = "Свернуть"; break; + case ChromeKind.Maximize: text = _isMaximized ? "Свернуть в окно" : "Развернуть"; break; + case ChromeKind.Close: text = "Закрыть"; break; + } + + if (text == null) + { + _tip.Hide(this); + return; + } + + var rect = GetButtonRect(kind); + _tip.Show(text, this, rect.X + rect.Width / 2, Height + 2, 1500); + } + + private static int BlendChannel(int from, int to, float amount) + { + return (int)(from + (to - from) * amount); + } + + [DllImport("user32.dll")] + private static extern bool ReleaseCapture(); + + [DllImport("user32.dll")] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + } +} diff --git a/src/Elfisa.UI/Controls/ModernUploadPanel.cs b/src/Elfisa.UI/Controls/ModernUploadPanel.cs index adade12..8713f47 100644 --- a/src/Elfisa.UI/Controls/ModernUploadPanel.cs +++ b/src/Elfisa.UI/Controls/ModernUploadPanel.cs @@ -8,15 +8,15 @@ using Elfisa.UI.Theming; namespace Elfisa.UI.Controls { /// - /// Экран «Загрузить» — карточка с пунктирной рамкой и кнопками (Pharma Trust). + /// Экран «Загрузить» — компактная карточка по центру с действиями синхронизации. /// public class ModernUploadPanel : Panel { private readonly ModernButton _uploadBtn; private readonly ModernButton _downloadBtn; private ThemeColors _theme; - private string _title = "Загрузка прайс-листа и синхронизация"; - private string _subtitle = "Импорт PriceList.xml или обмен данными с сервером поставщика."; + private string _title = "Синхронизация прайс-листа"; + private string _subtitle = "Обмен данными с сервером: скачайте актуальный прайс или отправьте локальные изменения."; public ModernButton UploadButton => _uploadBtn; public ModernButton DownloadButton => _downloadBtn; @@ -47,16 +47,18 @@ namespace Elfisa.UI.Controls Name = "uploadBtn", Text = "Загрузить на сервер", ButtonStyle = ModernButtonStyle.Primary, - Width = 220, - Height = 40 + Width = 200, + Height = 40, + AllowTextEllipsis = false }; _downloadBtn = new ModernButton { Name = "downloadBtn", Text = "Скачать с сервера", ButtonStyle = ModernButtonStyle.Secondary, - Width = 220, - Height = 40 + Width = 200, + Height = 40, + AllowTextEllipsis = false }; Controls.Add(_downloadBtn); @@ -78,7 +80,7 @@ namespace Elfisa.UI.Controls var card = GetCardRect(); var totalWidth = _uploadBtn.Width + 12 + _downloadBtn.Width; var left = card.X + Math.Max(24, (card.Width - totalWidth) / 2); - var top = card.Bottom - 64; + var top = card.Y + 168; _uploadBtn.Location = new Point(left, top); _downloadBtn.Location = new Point(left + _uploadBtn.Width + 12, top); @@ -86,12 +88,15 @@ namespace Elfisa.UI.Controls private Rectangle GetCardRect() { - var margin = 20; + const int cardW = 560; + const int cardH = 240; + var w = Math.Min(cardW, Math.Max(320, Width - 48)); + var h = Math.Min(cardH, Math.Max(200, Height - 48)); return new Rectangle( - margin, - margin, - Math.Max(200, Width - margin * 2), - Math.Max(200, Height - margin * 2)); + Math.Max(24, (Width - w) / 2), + Math.Max(24, (Height - h) / 2), + w, + h); } protected override void OnPaint(PaintEventArgs e) @@ -102,44 +107,52 @@ namespace Elfisa.UI.Controls g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; var card = GetCardRect(); - using (var fill = new SolidBrush(theme.AccentPrimarySoft)) + using (var shadow = new SolidBrush(Color.FromArgb(18, theme.TextPrimary))) + { + GraphicsExtensions.FillRoundedRectangle( + g, + shadow, + new Rectangle(card.X + 3, card.Y + 4, card.Width, card.Height), + theme.CornerRadiusMedium); + } + + using (var fill = new SolidBrush(theme.Surface)) { GraphicsExtensions.FillRoundedRectangle(g, fill, card, theme.CornerRadiusMedium); } - using (var pen = new Pen(theme.AccentPrimaryBorder, 2f)) + using (var pen = new Pen(theme.Border, 1f)) { - pen.DashStyle = DashStyle.Dash; GraphicsExtensions.DrawRoundedRectangle(g, pen, card, theme.CornerRadiusMedium); } - var iconRect = new Rectangle(card.X + (card.Width - 40) / 2, card.Y + 28, 40, 40); - using (var iconBrush = new SolidBrush(Color.FromArgb(40, theme.Success))) + var iconRect = new Rectangle(card.X + (card.Width - 44) / 2, card.Y + 28, 44, 44); + using (var iconBrush = new SolidBrush(theme.AccentPrimarySoft)) { - GraphicsExtensions.FillRoundedRectangle(g, iconBrush, iconRect, 10); + GraphicsExtensions.FillRoundedRectangle(g, iconBrush, iconRect, 12); } - using (var iconFont = new Font("Segoe MDL2 Assets", 20f)) + using (var iconFont = new Font("Segoe MDL2 Assets", 18f)) { TextRenderer.DrawText( g, "\uE896", iconFont, iconRect, - theme.Success, + theme.AccentPrimary, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine); } - var titleRect = new Rectangle(card.X + 24, card.Y + 78, card.Width - 48, 32); + var titleRect = new Rectangle(card.X + 28, card.Y + 84, card.Width - 56, 28); TextRenderer.DrawText( g, _title, theme.FontSemibold16, titleRect, - theme.Success, + theme.TextPrimary, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine | TextFormatFlags.EndEllipsis); - var subRect = new Rectangle(card.X + 32, card.Y + 112, card.Width - 64, 48); + var subRect = new Rectangle(card.X + 36, card.Y + 116, card.Width - 72, 44); TextRenderer.DrawText( g, _subtitle, diff --git a/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs b/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs index 38c9ee6..85dc67d 100644 --- a/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs +++ b/src/Elfisa.UI/Helpers/SkillGradientButtonPaint.cs @@ -1,11 +1,12 @@ using System.Drawing; using System.Drawing.Drawing2D; using Elfisa.UI.Controls; +using Elfisa.UI.Theming; namespace Elfisa.UI.Helpers { /// - /// Градиент из skill winforms-modern-buttons для ModernButton (без тяжёлой тени — она давала артефакты на toolbar). + /// Градиент для Primary/Danger в тон активной палитры (Clinical Pro teal, не «Google blue»). /// internal static class SkillGradientButtonPaint { @@ -26,41 +27,45 @@ namespace Elfisa.UI.Helpers out Color top, out Color bottom) { + var theme = ThemeManager.Colors; + if (style == ModernButtonStyle.Danger) { + var danger = theme.Danger; if (pressed) { - top = Color.FromArgb(175, 30, 30); - bottom = Color.FromArgb(140, 20, 20); + top = GraphicsExtensions.Blend(danger, Color.Black, 0.22f); + bottom = GraphicsExtensions.Blend(danger, Color.Black, 0.32f); } else if (hover) { - top = Color.FromArgb(245, 110, 110); - bottom = Color.FromArgb(215, 55, 55); + top = GraphicsExtensions.Blend(danger, Color.White, 0.18f); + bottom = danger; } else { - top = Color.FromArgb(235, 87, 87); - bottom = Color.FromArgb(200, 40, 40); + top = GraphicsExtensions.Blend(danger, Color.White, 0.08f); + bottom = GraphicsExtensions.Blend(danger, Color.Black, 0.12f); } return; } + // Primary — мягкий teal шапки, без синего «крика». if (pressed) { - top = Color.FromArgb(25, 70, 160); - bottom = Color.FromArgb(15, 55, 130); + top = theme.AccentPrimaryPressed; + bottom = GraphicsExtensions.Blend(theme.AccentPrimaryPressed, Color.Black, 0.12f); } else if (hover) { - top = Color.FromArgb(90, 150, 255); - bottom = Color.FromArgb(45, 105, 220); + top = theme.AccentPrimaryHover; + bottom = theme.AccentPrimary; } else { - top = Color.FromArgb(66, 133, 244); - bottom = Color.FromArgb(30, 87, 194); + top = theme.TitleBarBottom; + bottom = theme.TitleBarTop; } } @@ -73,6 +78,7 @@ namespace Elfisa.UI.Helpers bool pressed, bool enabled) { + var theme = ThemeManager.Colors; var radius = GetCornerRadius(style, compactIconMode); var buttonRect = new Rectangle( clientRect.X, @@ -86,7 +92,7 @@ namespace Elfisa.UI.Helpers if (!enabled) { - using (var brush = new SolidBrush(Color.FromArgb(220, 230, 235))) + using (var brush = new SolidBrush(GraphicsExtensions.Blend(theme.Border, theme.Surface, 0.35f))) { GraphicsExtensions.FillRoundedRectangle(g, brush, buttonRect, radius); } diff --git a/src/Elfisa.UI/Helpers/WindowChromeHelper.cs b/src/Elfisa.UI/Helpers/WindowChromeHelper.cs index c2a9d5f..75c5b9c 100644 --- a/src/Elfisa.UI/Helpers/WindowChromeHelper.cs +++ b/src/Elfisa.UI/Helpers/WindowChromeHelper.cs @@ -46,7 +46,7 @@ namespace Elfisa.UI.Helpers TrySetInt(form.Handle, DwmwaUseImmersiveDarkMode, useDarkMode); TrySetInt(form.Handle, DwmwaUseImmersiveDarkModeLegacy, useDarkMode); - var caption = theme.TitleBarTop; + var caption = theme.AccentPrimary; var text = IsLightCaption(caption) ? Color.Black : Color.White; TrySetUInt(form.Handle, DwmwaCaptionColor, ColorToDwm(caption)); @@ -55,6 +55,41 @@ namespace Elfisa.UI.Helpers RefreshNonClientArea(form); } + /// + /// Обработка ресайза для FormBorderStyle.None (края окна). + /// + public static bool TryHandleNcHitTest(Form form, ref Message m, int border = 6) + { + const int wmNcHitTest = 0x84; + if (m.Msg != wmNcHitTest || form == null) + { + return false; + } + + var screen = new Point(m.LParam.ToInt32() & 0xFFFF, (m.LParam.ToInt32() >> 16) & 0xFFFF); + // signed short for multi-monitor negative coords + screen = new Point((short)(m.LParam.ToInt32() & 0xFFFF), (short)((m.LParam.ToInt32() >> 16) & 0xFFFF)); + var client = form.PointToClient(screen); + var w = form.ClientSize.Width; + var h = form.ClientSize.Height; + + var left = client.X <= border; + var right = client.X >= w - border; + var top = client.Y <= border; + var bottom = client.Y >= h - border; + + if (top && left) { m.Result = (IntPtr)13; return true; } // HTTOPLEFT + if (top && right) { m.Result = (IntPtr)14; return true; } // HTTOPRIGHT + if (bottom && left) { m.Result = (IntPtr)16; return true; } // HTBOTTOMLEFT + if (bottom && right) { m.Result = (IntPtr)17; return true; } // HTBOTTOMRIGHT + if (left) { m.Result = (IntPtr)10; return true; } // HTLEFT + if (right) { m.Result = (IntPtr)11; return true; } // HTRIGHT + if (top) { m.Result = (IntPtr)12; return true; } // HTTOP + if (bottom) { m.Result = (IntPtr)15; return true; } // HTBOTTOM + + return false; + } + public static void ApplyThemedDialogHeader(Panel headerPanel, Label headerLabel, ThemeColors theme) { if (theme == null) diff --git a/src/Elfisa.UI/Theming/ClinicalProPalette.cs b/src/Elfisa.UI/Theming/ClinicalProPalette.cs index fc83f14..44d63a1 100644 --- a/src/Elfisa.UI/Theming/ClinicalProPalette.cs +++ b/src/Elfisa.UI/Theming/ClinicalProPalette.cs @@ -26,57 +26,57 @@ namespace Elfisa.UI.Theming AccentPrimary = Color.FromArgb(15, 118, 110), AccentPrimaryHover = Color.FromArgb(13, 148, 136), AccentPrimaryPressed = Color.FromArgb(17, 94, 89), - AccentPrimarySoft = Color.FromArgb(236, 253, 245), + AccentPrimarySoft = Color.FromArgb(240, 253, 250), AccentPrimaryBorder = Color.FromArgb(153, 246, 228), AccentSecondary = Color.White, Success = Color.FromArgb(5, 150, 105), SuccessSoft = Color.FromArgb(209, 250, 229), Warning = Color.FromArgb(217, 119, 6), WarningSoft = Color.FromArgb(254, 243, 199), - Danger = Color.FromArgb(225, 29, 72), - DangerSoft = Color.FromArgb(255, 241, 242), + Danger = Color.FromArgb(190, 58, 82), + DangerSoft = Color.FromArgb(255, 245, 247), Info = Color.FromArgb(8, 145, 178), InfoSoft = Color.FromArgb(224, 242, 254), - TitleBarTop = Color.FromArgb(17, 94, 89), - TitleBarBottom = Color.FromArgb(15, 118, 110), + TitleBarTop = Color.FromArgb(15, 118, 110), + TitleBarBottom = Color.FromArgb(13, 148, 136), NavBackground = Color.White, - CartHeaderBackground = Color.FromArgb(236, 253, 245), - ToolbarBackground = Color.FromArgb(252, 253, 253), + CartHeaderBackground = Color.FromArgb(240, 253, 250), + ToolbarBackground = Color.FromArgb(248, 250, 252), SectionHeaderBackground = Color.FromArgb(241, 245, 249), SidebarBackground = Color.White, SidebarItem = Color.FromArgb(100, 116, 139), SidebarItemHover = Color.FromArgb(248, 250, 252), - SidebarItemActive = Color.FromArgb(236, 253, 245), + SidebarItemActive = Color.FromArgb(204, 251, 241), SidebarItemActiveText = Color.FromArgb(15, 118, 110), GridHeaderBackground = Color.FromArgb(241, 245, 249), - GridHeaderForeground = Color.FromArgb(100, 116, 139), + GridHeaderForeground = Color.FromArgb(71, 85, 105), GridRowBackground = Color.White, GridRowAlternate = Color.FromArgb(248, 250, 252), - GridRowHover = Color.FromArgb(248, 250, 252), + GridRowHover = Color.FromArgb(240, 253, 250), GridRowSelected = Color.FromArgb(15, 118, 110), GridRowSelectedText = Color.White, - GridBorder = Color.FromArgb(241, 245, 249), + GridBorder = Color.FromArgb(226, 232, 240), InputBackground = Color.White, - InputBorder = Color.FromArgb(226, 232, 240), + InputBorder = Color.FromArgb(203, 213, 225), InputBorderFocus = Color.FromArgb(20, 184, 166), OverlayScrim = Color.FromArgb(160, 15, 23, 42), Shadow = Color.FromArgb(28, 15, 23, 42), - ScrollbarThumb = Color.FromArgb(203, 213, 225), + ScrollbarThumb = Color.FromArgb(148, 163, 184), ScrollbarTrack = Color.FromArgb(248, 250, 252), CornerRadiusSmall = 8, - CornerRadiusMedium = 10, - GridRowHeight = 38, - GridHeaderHeight = 40 + CornerRadiusMedium = 12, + GridRowHeight = 36, + GridHeaderHeight = 38 }; public ClinicalProPalette() { - Colors.FontRegular12 = new Font("Segoe UI", 12f); - Colors.FontRegular13 = new Font("Segoe UI", 13f); - Colors.FontRegular14 = new Font("Segoe UI", 14f); - Colors.FontSemibold14 = new Font("Segoe UI Semibold", 14f); - Colors.FontSemibold16 = new Font("Segoe UI Semibold", 16f); - Colors.FontBold18 = new Font("Segoe UI", 18f, FontStyle.Bold); + Colors.FontRegular12 = new Font("Segoe UI", 11f); + Colors.FontRegular13 = new Font("Segoe UI", 12f); + Colors.FontRegular14 = new Font("Segoe UI", 13f); + Colors.FontSemibold14 = new Font("Segoe UI Semibold", 13f); + Colors.FontSemibold16 = new Font("Segoe UI Semibold", 15f); + Colors.FontBold18 = new Font("Segoe UI Semibold", 17f, FontStyle.Bold); } } } diff --git a/src/Elfisa.UI/Theming/ThemeManager.cs b/src/Elfisa.UI/Theming/ThemeManager.cs index d9edcfb..cc62630 100644 --- a/src/Elfisa.UI/Theming/ThemeManager.cs +++ b/src/Elfisa.UI/Theming/ThemeManager.cs @@ -17,7 +17,7 @@ namespace Elfisa.UI.Theming { ThemeVariant.DarkProductivity, new DarkProductivityPalette() } }; - private static ThemeVariant _currentVariant = ThemeVariant.FluentWin11; + private static ThemeVariant _currentVariant = ThemeVariant.ClinicalPro; public static event EventHandler ThemeChanged; @@ -69,7 +69,7 @@ namespace Elfisa.UI.Theming public static void ToggleProductivityTheme() { CurrentVariant = IsDarkProductivityActive - ? ThemeVariant.PharmaTrust + ? ThemeVariant.ClinicalPro : ThemeVariant.DarkProductivity; } }