Release v1.0.21: consignees from web, remove test pharmacies.
This commit is contained in:
parent
166a431bda
commit
15dcf70bae
5
.gitignore
vendored
5
.gitignore
vendored
@ -20,3 +20,8 @@ Desktop.ini
|
||||
efClient.db
|
||||
*.db-journal
|
||||
**/user.config
|
||||
|
||||
packages/
|
||||
installer/output/
|
||||
dist/
|
||||
|
||||
|
||||
110
installer/ElfisaPharmacy.iss
Normal file
110
installer/ElfisaPharmacy.iss
Normal file
@ -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"
|
||||
24
installer/README.md
Normal file
24
installer/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Установщик ЭльФиСА
|
||||
|
||||
## Публичная ссылка (CDN)
|
||||
|
||||
https://cdn.24pharmdata.ru
|
||||
|
||||
При открытии корня CDN сразу скачивается установщик.
|
||||
|
||||
Стабильное имя файла:
|
||||
|
||||
https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe
|
||||
|
||||
## Локальная сборка
|
||||
|
||||
- `installer/output/ElfisaPharmacy-Setup-1.0.20.exe`
|
||||
- `installer/output/ElfisaPharmacy-Setup.exe` (копия для CDN)
|
||||
|
||||
## Пересборка
|
||||
|
||||
```powershell
|
||||
.\scripts\build-installer.ps1
|
||||
```
|
||||
|
||||
После сборки залейте `ElfisaPharmacy-Setup.exe` на CDN (или сделайте его index/default document).
|
||||
BIN
installer/assets/elfisa-logo.png
Normal file
BIN
installer/assets/elfisa-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
installer/assets/elfisa.ico
Normal file
BIN
installer/assets/elfisa.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
BIN
installer/assets/wizard-side.bmp
Normal file
BIN
installer/assets/wizard-side.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 201 KiB |
BIN
installer/assets/wizard-small.bmp
Normal file
BIN
installer/assets/wizard-small.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
18
installer/license.txt
Normal file
18
installer/license.txt
Normal file
@ -0,0 +1,18 @@
|
||||
ЛИЦЕНЗИОННОЕ СОГЛАШЕНИЕ
|
||||
Электронная Фармация (ЭльФиСА)
|
||||
|
||||
1. Программа предоставляется для использования в рамках договора
|
||||
с поставщиком услуг PharmData / 24pharmdata.
|
||||
|
||||
2. Запрещается передавать третьим лицам учётные данные доступа,
|
||||
а также модифицировать клиент способом, нарушающим безопасность
|
||||
обмена данными.
|
||||
|
||||
3. Пользователь несёт ответственность за корректность оформляемых
|
||||
заказов и за сохранность локальных данных на рабочем месте.
|
||||
|
||||
4. Устанавливая программу, вы подтверждаете согласие с условиями
|
||||
использования сервиса и политикой обработки данных, действующими
|
||||
у оператора сервиса.
|
||||
|
||||
При возникновении вопросов обратитесь в службу поддержки.
|
||||
20
installer/welcome.txt
Normal file
20
installer/welcome.txt
Normal file
@ -0,0 +1,20 @@
|
||||
ЭльФиСА — Электронная Фармация
|
||||
================================
|
||||
|
||||
Добро пожаловать в установку настольного клиента «Электронная Фармация».
|
||||
|
||||
Программа предназначена для работы аптек с прайс-листами поставщиков,
|
||||
заказами, накладными и обменом данными с сервером PharmData.
|
||||
|
||||
Скачать установщик всегда можно по адресу:
|
||||
https://cdn.24pharmdata.ru
|
||||
|
||||
Установщик:
|
||||
• скопирует программу в выбранную папку;
|
||||
• предложит ярлык в меню «Пуск»;
|
||||
• по желанию создаст ярлык на рабочем столе.
|
||||
|
||||
Рекомендуется закрыть запущенную «Электронную Фармацию» перед установкой.
|
||||
После установки при первом запуске выполните вход в систему.
|
||||
|
||||
© PharmData / ЭльФиСА
|
||||
48
scripts/build-installer.ps1
Normal file
48
scripts/build-installer.ps1
Normal file
@ -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"
|
||||
@ -374,6 +374,30 @@ namespace Электронная_Фармация.Classes
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<BuyerLocationDto>> GetBuyerLocationsAsync()
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
var responseBody = await SendAuthorizedGetAsync("/api/buyer/locations", "загрузку грузополучателей");
|
||||
try
|
||||
{
|
||||
using (var doc = JsonDocument.Parse(responseBody))
|
||||
{
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<BuyerLocationDto>>(responseBody, JsonOptions)
|
||||
?? new List<BuyerLocationDto>();
|
||||
}
|
||||
}
|
||||
|
||||
return new List<BuyerLocationDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Error("ApiClient", "Не удалось разобрать грузополучателей", ex);
|
||||
throw new HttpRequestException($"Некорректный ответ сервера при загрузке грузополучателей: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BuyerOrderDetailDto> GetBuyerOrderByIdAsync(string orderId)
|
||||
{
|
||||
EnsureAuthenticated();
|
||||
@ -734,6 +758,27 @@ namespace Электронная_Фармация.Classes
|
||||
public List<BuyerOrderListItemDto> 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")]
|
||||
|
||||
@ -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";
|
||||
/// <summary>Публичная раздача установщика (корень CDN отдаёт Setup.exe).</summary>
|
||||
public const string DefaultInstallerCdnUrl = "https://cdn.24pharmdata.ru";
|
||||
public const string DefaultInstallerDownloadUrl = "https://cdn.24pharmdata.ru/ElfisaPharmacy-Setup.exe";
|
||||
private const string LegacyApiBaseUrl = "http://195.34.241.84:9988";
|
||||
private const string ApiBaseUrlEnvVar = "ELF_API_BASE_URL";
|
||||
private static bool _sqliteInteropPrepared;
|
||||
private static readonly object _sqliteInteropLock = new object();
|
||||
|
||||
public static string InstallerDownloadUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
var fromEnv = Environment.GetEnvironmentVariable("ELF_INSTALLER_URL");
|
||||
if (!string.IsNullOrWhiteSpace(fromEnv))
|
||||
{
|
||||
return fromEnv.Trim();
|
||||
}
|
||||
|
||||
return DefaultInstallerDownloadUrl;
|
||||
}
|
||||
}
|
||||
|
||||
public static string InstallerCdnUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
var fromEnv = Environment.GetEnvironmentVariable("ELF_CDN_URL");
|
||||
if (!string.IsNullOrWhiteSpace(fromEnv))
|
||||
{
|
||||
return fromEnv.Trim().TrimEnd('/');
|
||||
}
|
||||
|
||||
return DefaultInstallerCdnUrl;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces known dead API URLs in saved user settings.
|
||||
/// </summary>
|
||||
@ -97,24 +128,77 @@ namespace Электронная_Фармация.Classes
|
||||
return url;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writable per-user data (DB, logs). Program Files is read-only for normal users.
|
||||
/// </summary>
|
||||
public static string UserDataDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
var dir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"ЭльФиСА",
|
||||
"Электронная Фармация");
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Caller will surface a clearer error when opening the DB.
|
||||
}
|
||||
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
public static string LogsDirectory => Path.Combine(UserDataDirectory, "Logs");
|
||||
|
||||
public static string SqliteDbPath
|
||||
{
|
||||
get
|
||||
{
|
||||
MigrateLegacySqliteDatabaseIfNeeded();
|
||||
return Path.Combine(UserDataDirectory, "efClient.db");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One-time copy from install folder (old portable layout) into LocalAppData.
|
||||
/// </summary>
|
||||
private static void MigrateLegacySqliteDatabaseIfNeeded()
|
||||
{
|
||||
try
|
||||
{
|
||||
var target = Path.Combine(UserDataDirectory, "efClient.db");
|
||||
if (File.Exists(target))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
var 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")
|
||||
};
|
||||
|
||||
foreach (var source in candidates)
|
||||
{
|
||||
if (!File.Exists(source))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (File.Exists(data))
|
||||
{
|
||||
return data;
|
||||
File.Copy(source, target, overwrite: false);
|
||||
AppDebugLog.Info("AppConfig", $"SQLite DB migrated: {source} -> {target}");
|
||||
return;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppDebugLog.Info("AppConfig", $"SQLite DB migrate skipped: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ namespace Электронная_Фармация.Classes
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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");
|
||||
|
||||
@ -322,6 +322,131 @@ namespace Электронная_Фармация.Classes
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Подтягивает грузополучателей с сервера (BuyerLocation) в локальную таблицу Consignees.
|
||||
/// Источник истины — веб-кабинет; локально только кэш + выбор активной аптеки.
|
||||
/// </summary>
|
||||
public static int SyncFromServer(IList<BuyerLocationDto> locations)
|
||||
{
|
||||
if (locations == null || locations.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var upserted = 0;
|
||||
using (var connection = new SQLiteConnection(AppConfig.SqliteConnectionString))
|
||||
{
|
||||
connection.Open();
|
||||
using (var tx = connection.BeginTransaction())
|
||||
{
|
||||
// Сбрасываем default — выставим по данным сервера.
|
||||
using (var clearDefault = new SQLiteCommand(
|
||||
"UPDATE Consignees SET ConsigneesUseAsDefault = 0",
|
||||
connection,
|
||||
tx))
|
||||
{
|
||||
clearDefault.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
int nextCode;
|
||||
using (var nextCodeCmd = new SQLiteCommand(
|
||||
"SELECT ifnull(MAX(codeConsignees), 0) + 1 FROM Consignees",
|
||||
connection,
|
||||
tx))
|
||||
{
|
||||
nextCode = Convert.ToInt32(nextCodeCmd.ExecuteScalar());
|
||||
}
|
||||
|
||||
foreach (var loc in locations)
|
||||
{
|
||||
var locationId = (loc.BuyerLocationID ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(locationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var address = (loc.Address ?? string.Empty).Trim();
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
{
|
||||
address = "Грузополучатель";
|
||||
}
|
||||
|
||||
var name = address;
|
||||
if (name.Length > 120)
|
||||
{
|
||||
name = name.Substring(0, 120);
|
||||
}
|
||||
|
||||
long? existingId = null;
|
||||
using (var find = new SQLiteCommand(
|
||||
@"SELECT idConsignees FROM Consignees
|
||||
WHERE lower(trim(LocationId)) = lower(@loc)
|
||||
LIMIT 1",
|
||||
connection,
|
||||
tx))
|
||||
{
|
||||
find.Parameters.AddWithValue("@loc", locationId);
|
||||
var found = find.ExecuteScalar();
|
||||
if (found != null && found != DBNull.Value)
|
||||
{
|
||||
existingId = Convert.ToInt64(found);
|
||||
}
|
||||
}
|
||||
|
||||
if (existingId.HasValue)
|
||||
{
|
||||
using (var upd = new SQLiteCommand(
|
||||
@"UPDATE Consignees
|
||||
SET ConsigneesName = @name,
|
||||
ConsigneesAddress = @address,
|
||||
ConsigneesUseAsDefault = @isDefault,
|
||||
LocationId = @loc
|
||||
WHERE idConsignees = @id",
|
||||
connection,
|
||||
tx))
|
||||
{
|
||||
upd.Parameters.AddWithValue("@name", name);
|
||||
upd.Parameters.AddWithValue("@address", address);
|
||||
upd.Parameters.AddWithValue("@isDefault", loc.IsDefault ? 1 : 0);
|
||||
upd.Parameters.AddWithValue("@loc", locationId);
|
||||
upd.Parameters.AddWithValue("@id", existingId.Value);
|
||||
upd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var ins = new SQLiteCommand(
|
||||
@"INSERT INTO Consignees
|
||||
(codeConsignees, ConsigneesName, ConsigneesAddress, ConsigneesUseAsDefault, LocationId)
|
||||
VALUES (@code, @name, @address, @isDefault, @loc)",
|
||||
connection,
|
||||
tx))
|
||||
{
|
||||
ins.Parameters.AddWithValue("@code", nextCode++);
|
||||
ins.Parameters.AddWithValue("@name", name);
|
||||
ins.Parameters.AddWithValue("@address", address);
|
||||
ins.Parameters.AddWithValue("@isDefault", loc.IsDefault ? 1 : 0);
|
||||
ins.Parameters.AddWithValue("@loc", locationId);
|
||||
ins.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
upserted++;
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
|
||||
var active = GetDefaultConsigneeName();
|
||||
if (!string.IsNullOrWhiteSpace(active))
|
||||
{
|
||||
SetActiveConsignee(active);
|
||||
}
|
||||
|
||||
return upserted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Синхронизирует поля заказа в `PriceList` (Zakaz/SummaZakaza) с корзиной выбранной аптеки.
|
||||
/// Данные корзины хранятся в TempOrderItems с фильтром по ConsigneeName, а PriceList обновляется
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
<GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources>
|
||||
<NuGetPackageImportStamp>
|
||||
</NuGetPackageImportStamp>
|
||||
<ApplicationIcon>Resources\elfisa.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@ -353,4 +354,7 @@
|
||||
<Import Project="packages\EntityFramework.6.4.4\build\EntityFramework.targets" Condition="Exists('packages\EntityFramework.6.4.4\build\EntityFramework.targets')" />
|
||||
<Import Project="packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets" Condition="Exists('packages\System.ValueTuple.4.6.1\build\net471\System.ValueTuple.targets')" />
|
||||
<Import Project="packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets" Condition="Exists('packages\System.Resources.Extensions.8.0.0\buildTransitive\net462\System.Resources.Extensions.targets')" />
|
||||
<ItemGroup>
|
||||
<None Include="Resources\elfisa.ico" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
7
src/ElectronicPharmacy/Form1.Designer.cs
generated
7
src/ElectronicPharmacy/Form1.Designer.cs
generated
@ -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 = "Электронная Фармация";
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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())
|
||||
{
|
||||
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
|
||||
{
|
||||
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);
|
||||
await SyncFromServerAsync(silent: false);
|
||||
}
|
||||
|
||||
// Всегда зажимаем в границы формы после вычисления ширины 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)
|
||||
private async Task SyncFromServerAsync(bool silent)
|
||||
{
|
||||
try
|
||||
{
|
||||
btnAdd.Enabled = false;
|
||||
var client = new ApiClient();
|
||||
if (!client.IsAuthenticated)
|
||||
{
|
||||
var token = Settings.Default.stringToken;
|
||||
if (string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
if (!silent)
|
||||
{
|
||||
UiDialogs.ShowInfo(
|
||||
"Сначала войдите на сервер — грузополучатели подтягиваются из веб-кабинета покупателя.",
|
||||
"Грузополучатели",
|
||||
this);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
client.SetToken(token);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,36 +1,37 @@
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>. <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>.
|
||||
[assembly: AssemblyTitle("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>")]
|
||||
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ. пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ,
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ.
|
||||
[assembly: AssemblyTitle("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>")]
|
||||
[assembly: AssemblyCopyright("Copyright <EFBFBD> 2023")]
|
||||
[assembly: AssemblyProduct("пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ")]
|
||||
[assembly: AssemblyCopyright("Copyright пїЅ 2023")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> False <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ComVisible <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> COM. <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>
|
||||
// COM, <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> ComVisible <20> TRUE <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>.
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ False пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
||||
// пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ COM. пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ
|
||||
// COM, пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ ComVisible пїЅ TRUE пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> GUID <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD>, <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> COM
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ GUID пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ, пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ COM
|
||||
[assembly: Guid("145d8060-4187-4871-b4fb-8df795727b66")]
|
||||
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>:
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ:
|
||||
//
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ
|
||||
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
||||
//
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> "*", <20><><EFBFBD> <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><EFBFBD>:
|
||||
// пїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅ пїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ
|
||||
// пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ "*", пїЅпїЅпїЅ пїЅпїЅпїЅпїЅпїЅпїЅпїЅпїЅ пїЅпїЅпїЅпїЅ:
|
||||
// [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")]
|
||||
|
||||
|
||||
@ -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("")]
|
||||
|
||||
@ -29,6 +29,9 @@
|
||||
<Setting Name="UseDarkTheme" Type="System.Boolean" Scope="User">
|
||||
<Value Profile="(Default)">False</Value>
|
||||
</Setting>
|
||||
<Setting Name="PriceListSplitterDistance" Type="System.Int32" Scope="User">
|
||||
<Value Profile="(Default)">0</Value>
|
||||
</Setting>
|
||||
<Setting Name="InvoiceExportPath" Type="System.String" Scope="User">
|
||||
<Value Profile="(Default)" />
|
||||
</Setting>
|
||||
|
||||
BIN
src/ElectronicPharmacy/Resources/elfisa.ico
Normal file
BIN
src/ElectronicPharmacy/Resources/elfisa.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
@ -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
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Поиск остаётся сверху как в оригинале; контент явно ниже — без Dock-наложения.
|
||||
/// </summary>
|
||||
private void EnsureChromeLayout()
|
||||
{
|
||||
// Убираем старую dock-панель, если осталась от прошлых сборок в той же сессии.
|
||||
var oldToolbar = Controls["priceToolbar"];
|
||||
if (oldToolbar != null)
|
||||
{
|
||||
var toRestore = oldToolbar.Controls.Cast<Control>().ToArray();
|
||||
foreach (var control in toRestore)
|
||||
{
|
||||
oldToolbar.Controls.Remove(control);
|
||||
Controls.Add(control);
|
||||
control.BringToFront();
|
||||
}
|
||||
|
||||
Controls.Remove(oldToolbar);
|
||||
oldToolbar.Dispose();
|
||||
}
|
||||
|
||||
// Строка наценки скрыта — проценты берутся из настроек/дефолта без UI-прослойки.
|
||||
if (TLPPriceList.RowStyles.Count >= 2)
|
||||
{
|
||||
TLPPriceList.RowStyles[1].SizeType = SizeType.Absolute;
|
||||
TLPPriceList.RowStyles[1].Height = 0;
|
||||
}
|
||||
|
||||
panel1.Visible = false;
|
||||
panel1.Height = 0;
|
||||
|
||||
SplitPriceList.Panel1MinSize = 120;
|
||||
SplitPriceList.Panel2MinSize = 80;
|
||||
SplitPriceList.IsSplitterFixed = false;
|
||||
SplitPriceList.FixedPanel = FixedPanel.None;
|
||||
SplitPriceList.SplitterWidth = 6;
|
||||
|
||||
// Восстановить положение сплиттера корзины, если пользователь уже двигал.
|
||||
RestoreCartSplitterDistance();
|
||||
SplitPriceList.SplitterMoved += (_, __) => PersistCartSplitterDistance();
|
||||
|
||||
dgvPriceList.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.None;
|
||||
dgvPriceList.ColumnHeadersVisible = true;
|
||||
dgvPriceList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing;
|
||||
if (dgvPriceList.ColumnHeadersHeight < 34)
|
||||
{
|
||||
dgvPriceList.ColumnHeadersHeight = 36;
|
||||
}
|
||||
|
||||
if (!POrderInfo.Controls.ContainsKey("cartHeader"))
|
||||
{
|
||||
var cartHeader = new ModernSectionHeader
|
||||
{
|
||||
Name = "cartHeader",
|
||||
Height = 32,
|
||||
HeaderText = "Корзина"
|
||||
};
|
||||
cartHeader.ApplyTheme(Elfisa.UI.Theming.ThemeManager.Colors, cartStyle: true);
|
||||
cartHeader.Dock = DockStyle.Top;
|
||||
POrderInfo.Controls.Add(cartHeader);
|
||||
tabOrderItems.SendToBack();
|
||||
cartHeader.BringToFront();
|
||||
}
|
||||
}
|
||||
|
||||
private void LayoutContentBelowSearch()
|
||||
{
|
||||
const int searchBand = 52;
|
||||
PContentPriceList.Dock = DockStyle.None;
|
||||
PContentPriceList.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
PContentPriceList.SetBounds(
|
||||
0,
|
||||
searchBand,
|
||||
Math.Max(100, ClientSize.Width),
|
||||
Math.Max(120, ClientSize.Height - searchBand));
|
||||
}
|
||||
|
||||
private void RestoreCartSplitterDistance()
|
||||
{
|
||||
try
|
||||
{
|
||||
var saved = Properties.Settings.Default.PriceListSplitterDistance;
|
||||
if (saved <= 0 || SplitPriceList.Height < 200)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var max = Math.Max(
|
||||
SplitPriceList.Panel1MinSize,
|
||||
SplitPriceList.Height - SplitPriceList.Panel2MinSize - SplitPriceList.SplitterWidth);
|
||||
SplitPriceList.SplitterDistance = Math.Max(SplitPriceList.Panel1MinSize, Math.Min(saved, max));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore until layout ready
|
||||
}
|
||||
}
|
||||
|
||||
private void PersistCartSplitterDistance()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (SplitPriceList.SplitterDistance > 80)
|
||||
{
|
||||
Properties.Settings.Default.PriceListSplitterDistance = SplitPriceList.SplitterDistance;
|
||||
Properties.Settings.Default.Save();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureBrowseSplit()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ using Elfisa.UI.Theming;
|
||||
namespace Elfisa.UI.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Верхняя синяя шапка как в оригинале: 3 равные кнопки, owner-draw + плавный hover (Fluent).
|
||||
/// Верхняя навигация: бренд + пункты (иконка сверху, подпись снизу).
|
||||
/// </summary>
|
||||
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<string> 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)
|
||||
{
|
||||
}
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
346
src/Elfisa.UI/Controls/ModernTitleBar.cs
Normal file
346
src/Elfisa.UI/Controls/ModernTitleBar.cs
Normal file
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Системная шапка: заголовок + свернуть/развернуть/закрыть.
|
||||
/// Иконки рисуются геометрией (не глифы) — как в Win11, без «ломаных» кнопок.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -8,15 +8,15 @@ using Elfisa.UI.Theming;
|
||||
namespace Elfisa.UI.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Экран «Загрузить» — карточка с пунктирной рамкой и кнопками (Pharma Trust).
|
||||
/// Экран «Загрузить» — компактная карточка по центру с действиями синхронизации.
|
||||
/// </summary>
|
||||
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,
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using Elfisa.UI.Controls;
|
||||
using Elfisa.UI.Theming;
|
||||
|
||||
namespace Elfisa.UI.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Градиент из skill winforms-modern-buttons для ModernButton (без тяжёлой тени — она давала артефакты на toolbar).
|
||||
/// Градиент для Primary/Danger в тон активной палитры (Clinical Pro teal, не «Google blue»).
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обработка ресайза для FormBorderStyle.None (края окна).
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user