diff --git a/.claude/agents/render-dxf.md b/.claude/agents/render-dxf.md
index f9f1d1a..be6e7b6 100644
--- a/.claude/agents/render-dxf.md
+++ b/.claude/agents/render-dxf.md
@@ -2,6 +2,7 @@
name: render-dxf
description: Specialista na kreslicí a DXF vrstvu Rozpisek (Rozpisky/Rendering/, Rozpisky/Dxf/). Použij pro změny v renderování (Skia, PNG, PDF, DXF výstup), ve čtečce DXF šablony, v mapování hodnot do polí, v kódech příloh a v práci s logy. Nepoužívej pro UI, ViewModely ani COM (CAD/Excel).
tools: Read, Edit, Write, Grep, Glob, Bash, PowerShell
+model: sonnet
---
Jsi specialista na kreslicí jádro aplikace Rozpisky (C# / .NET 10, česky psaný kód).
@@ -50,6 +51,6 @@ Needituj produkční data: `Rozpisky/Podklady/Rozpiska.dxf`, `vzorky/schema.dxf`
3. Ověř:
`dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
`dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q`
- (baseline je 50 zelených testů — nesmí ubýt).
+ (baseline je 55 zelených testů — nesmí ubýt).
4. V odpovědi vrať: co jsi změnil (soubor:řádek), proč, a výsledek buildu a testů. Když něco
neprošlo, řekni to rovnou i s chybovou hláškou — nezakrývej to.
diff --git a/.claude/agents/verifikator.md b/.claude/agents/verifikator.md
index 2f0c41b..d4a089f 100644
--- a/.claude/agents/verifikator.md
+++ b/.claude/agents/verifikator.md
@@ -2,6 +2,7 @@
name: verifikator
description: Read-only ověřovatel — spustí build a testy Rozpisek a vrátí strukturovaný verdikt. Použij po každé nezanedbatelné změně kódu, nebo když je potřeba nezávisle potvrdit, že projekt je v pořádku. Nic needituje.
tools: Read, Grep, Glob, Bash, PowerShell
+model: haiku
---
Jsi nezávislý ověřovatel projektu Rozpisky. **Nesmíš nic měnit** — nemáš nástroje na editaci
@@ -17,7 +18,7 @@ dotnet test "C:\Users\marek\_Osobní\C sharp\EXE\Rozpisky\Rozpisky.Tests\Rozpis
Testy spusť i tehdy, když build projde s varováními. Když build spadne, testy nespouštěj
a rovnou hlas chyby buildu.
-Baseline, se kterou porovnáváš: **build bez chyb a bez varování, 50 testů zelených, 0 přeskočených.**
+Baseline, se kterou porovnáváš: **build bez chyb a bez varování, 55 testů zelených, 0 přeskočených.**
Volitelně (jen když o to volající požádá) ověř, že aplikace naběhne:
spusť `Rozpisky\bin\Debug\net10.0-windows\Rozpisky.exe`, počkej ~8 s, zkontroluj, že proces
diff --git a/.claude/agents/wpf-ui.md b/.claude/agents/wpf-ui.md
index a8e165b..b56fec4 100644
--- a/.claude/agents/wpf-ui.md
+++ b/.claude/agents/wpf-ui.md
@@ -2,6 +2,7 @@
name: wpf-ui
description: Specialista na WPF prezentační vrstvu Rozpisek (ViewModels/, Views/, Themes/, Behaviors/, Converters/, Theming/). Použij pro změny obrazovek, XAML, bindingů, příkazů, chování gridů a vzhledu. Nepoužívej pro kreslicí/DXF jádro ani pro COM vrstvu.
tools: Read, Edit, Write, Grep, Glob, Bash, PowerShell
+model: sonnet
---
Jsi specialista na WPF vrstvu aplikace Rozpisky (C# / .NET 10, MVVM bez frameworku, česky psaný kód).
@@ -35,7 +36,7 @@ nebo `Rozpisky/Xlsx/` jen popiš v odpovědi a nech je na volajícím.
začneš měnit UI.
2. `MainViewModel.cs` je velký; hledej v něm grepem, nečti ho celý zbytečně.
3. Ověř: `dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
- `dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` (baseline 50 zelených).
+ `dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` (baseline 55 zelených).
Build musí být **bez varování** — chyby v XAML se často projeví jen jako varování.
4. WPF chyby v bindingu build nezachytí. Pokud změna může binding rozbít, řekni to explicitně
a doporuč ruční spuštění aplikace.
diff --git a/CLAUDE.md b/CLAUDE.md
index 993add9..67e1d74 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -11,7 +11,7 @@ dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
dotnet run --project "Rozpisky\Rozpisky.csproj"
```
-Baseline: build bez chyb a varování, **50 testů zelených**. Před hlášením hotovo obojí spustit
+Baseline: build bez chyb a varování, **55 testů zelených**. Před hlášením hotovo obojí spustit
(nebo použít `/overit`).
## Architektura — jediné pravidlo, které se nesmí porušit
@@ -38,8 +38,11 @@ jen relevantní kapitolu.
- Uživatelská data (číselníky, výběr CADu, logy) jdou přes
[`UzivatelskaData`](Rozpisky/Data/UzivatelskaData.cs) do `%APPDATA%\Rozpisky`. Do složky u `.exe`
se **nezapisuje** — může být jen pro čtení a přeinstalace ji přepíše. Číselníky navíc žijí ve dvou
- vrstvách ([`CiselnikyStore`](Rozpisky/Data/CiselnikyStore.cs)): dodávaná + uložený rozdíl
- uživatele. Změnu slučování vždy pokrýt testem — chyba tam se projeví tichou ztrátou dat.
+ vrstvách ([`CiselnikyStore`](Rozpisky/Data/CiselnikyStore.cs)) a loga ve dvou složkách
+ ([`LogaResolver`](Rozpisky/Dxf/LogaResolver.cs)): dodávané + uživatelské navrch.
+ **Při konfliktu vyhrává uživatelská vrstva** — jinak by se jeho úprava tiše neprojevila.
+ Výjimka jsou data, která drží správnost výstupu (`mapovani.json`, `Rozpiska.dxf`) — tam vyhrává
+ program. Změnu slučování vždy pokrýt testem: chyba tam se projeví tichou ztrátou dat.
## Rozvržení složky — tři druhy podkladů
diff --git a/Rozpisky.Tests/LogaVrstvyTesty.cs b/Rozpisky.Tests/LogaVrstvyTesty.cs
new file mode 100644
index 0000000..7e5af7f
--- /dev/null
+++ b/Rozpisky.Tests/LogaVrstvyTesty.cs
@@ -0,0 +1,80 @@
+using System.IO;
+using Rozpisky.Dxf;
+using Xunit;
+
+namespace Rozpisky.Tests;
+
+///
+/// Nabídka log ze dvou složek (): dodávaná vedle .exe
+/// a uživatelská, na kterou přeinstalace nesahá. Stejná hierarchie jako u číselníků –
+/// uživatelovo logo stejného jména přebíjí dodávané.
+///
+public class LogaVrstvyTesty : IDisposable
+{
+ private readonly string _koren = Path.Combine(Path.GetTempPath(), "rozpisky-loga-testy-" + Guid.NewGuid().ToString("N"));
+ private readonly string _dodavana;
+ private readonly string _uzivatelska;
+
+ public LogaVrstvyTesty()
+ {
+ _dodavana = Path.Combine(_koren, "dodavana");
+ _uzivatelska = Path.Combine(_koren, "uzivatelska");
+ Directory.CreateDirectory(_dodavana);
+ Directory.CreateDirectory(_uzivatelska);
+ }
+
+ public void Dispose()
+ {
+ try { Directory.Delete(_koren, recursive: true); } catch (IOException) { }
+ GC.SuppressFinalize(this);
+ }
+
+ private void Zaloz(string dir, params string[] soubory)
+ {
+ foreach (var s in soubory) File.WriteAllText(Path.Combine(dir, s), "0\nEOF\n");
+ }
+
+ [Fact]
+ public void BezUzivatelskychLogSeVratiJenDodavana()
+ {
+ Zaloz(_dodavana, "exprojekt.dxf", "mco.dxf");
+ Assert.Equal(new[] { "exprojekt.dxf", "mco.dxf" },
+ LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
+ }
+
+ [Fact]
+ public void UzivatelskaLogaSeDoplniKDodavanym()
+ {
+ Zaloz(_dodavana, "exprojekt.dxf");
+ Zaloz(_uzivatelska, "nova_firma.dxf");
+ Assert.Equal(new[] { "exprojekt.dxf", "nova_firma.dxf" },
+ LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
+ }
+
+ [Fact]
+ public void StejneJmenoSeVNabidceNezdvoji()
+ {
+ // Uživatel si opravil dodávané logo a uložil ho pod stejným jménem k sobě –
+ // v comboboxu musí zůstat jedna položka, ne dvě nerozlišitelné.
+ Zaloz(_dodavana, "exprojekt.dxf");
+ Zaloz(_uzivatelska, "exprojekt.dxf");
+ Assert.Equal(new[] { "exprojekt.dxf" }, LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
+ }
+
+ [Fact]
+ public void ChybejiciUzivatelskaSlozkaNevadi()
+ {
+ Zaloz(_dodavana, "exprojekt.dxf");
+ var neexistuje = Path.Combine(_koren, "tady-nic-neni");
+ Assert.Equal(new[] { "exprojekt.dxf" }, LogaResolver.SlozNabidku(neexistuje, _dodavana));
+ }
+
+ [Fact]
+ public void NabidkaJeAbecedniAIgnorujeJineNezDxf()
+ {
+ Zaloz(_dodavana, "tesia.dxf", "README.txt", "mco.dxf");
+ Zaloz(_uzivatelska, "alfa.dxf");
+ Assert.Equal(new[] { "alfa.dxf", "mco.dxf", "tesia.dxf" },
+ LogaResolver.SlozNabidku(_uzivatelska, _dodavana));
+ }
+}
diff --git a/Rozpisky/Dxf/LogaResolver.cs b/Rozpisky/Dxf/LogaResolver.cs
index 83c6673..89ae0d7 100644
--- a/Rozpisky/Dxf/LogaResolver.cs
+++ b/Rozpisky/Dxf/LogaResolver.cs
@@ -1,4 +1,5 @@
-using System.IO;
+using System.IO;
+using Rozpisky.Data;
using Rozpisky.Models;
namespace Rozpisky.Dxf;
@@ -8,19 +9,67 @@ namespace Rozpisky.Dxf;
/// Pravidla (kap. 4.3 manuálu SŽ):
///
/// Rozpiska_logo1/2 – financující instituce (dotační vlevo / státní vpravo),
-/// projektové, soubor ze složky loga\ vedle .exe,
+/// projektové, soubor ze složky loga\,
/// Rozpiska_logo3 – logo investora (číselník Organizace dle Projekt.Investor),
/// Rozpiska_logo4 – logo zhotovitele díla (číselník Firmy dle Projekt.ZhotovitelSOD),
/// Rozpiska_logo5 – logo zhotovitele objektu (číselník Firmy dle Priloha.ZhotovitelCasti),
/// Rozpiska_Orientační schéma – DXF u přílohy (Priloha.SchemaSoubor), relativně k .rzp.
///
/// Položky bez vyplněného / neexistujícího souboru se vynechají.
+///
+/// Loga se hledají ve dvou složkách se stejnou logikou jako číselníky: nejdřív
+/// loga\ v uživatelské složce (viz ), pak loga\ vedle
+/// .exe. Uživatelské logo stejného jména přebíjí dodávané. Bez toho by si uživatel, který přidá
+/// do číselníku firmu s vlastním logem, musel soubor kopírovat do instalační složky – tu ale
+/// přeinstalace přepíše a v Program Files do ní nejde psát.
+///
///
public static class LogaResolver
{
- /// Složka s logy dodávanými s programem (vedle .exe).
+ /// Složka s logy dodávanými s programem (vedle .exe). Jen ke čtení – instalace ji přepisuje.
public static string LogaDir => Path.Combine(AppContext.BaseDirectory, "loga");
+ /// Složka pro loga, která si přidal uživatel. Přeinstalace na ni nesahá.
+ public static string UzivatelskaLogaDir => UzivatelskaData.Cesta("loga");
+
+ /// Najde soubor loga: absolutní cesta se bere, jak je, jinak se hledá nejdřív mezi
+ /// uživatelskými logy a pak mezi dodávanými. Vrací null, když soubor nikde není.
+ public static string? Najdi(string? soubor)
+ {
+ if (string.IsNullOrWhiteSpace(soubor)) return null;
+ if (Path.IsPathRooted(soubor)) return File.Exists(soubor) ? soubor : null;
+
+ var uzivatelske = Path.Combine(UzivatelskaLogaDir, soubor);
+ if (File.Exists(uzivatelske)) return uzivatelske;
+
+ var dodavane = Path.Combine(LogaDir, soubor);
+ return File.Exists(dodavane) ? dodavane : null;
+ }
+
+ /// Jména dostupných log z obou složek dohromady, abecedně. Uživatelské logo stejného
+ /// jména dodávané nezdvojuje – v nabídce je jednou a použije se to uživatelovo.
+ public static IReadOnlyList DostupnaLoga() => SlozNabidku(UzivatelskaLogaDir, LogaDir);
+
+ /// Vlastní slučování nabídky – s cestami v parametru, ať jde otestovat bez profilu uživatele.
+ internal static List SlozNabidku(string uzivatelskaDir, string dodavanaDir)
+ {
+ var jmena = new SortedSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var dir in new[] { dodavanaDir, uzivatelskaDir })
+ {
+ try
+ {
+ if (!Directory.Exists(dir)) continue;
+ foreach (var soubor in Directory.GetFiles(dir, "*.dxf"))
+ jmena.Add(Path.GetFileName(soubor));
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ // Nedostupná složka (odpojený síťový disk apod.) nesmí shodit nabídku.
+ }
+ }
+ return jmena.ToList();
+ }
+
/// Složka aktuálního souboru .rzp (pro relativní cestu k schématu); null = neuloženo.
public static IReadOnlyDictionary Resolve(Projekt p, Priloha r, Ciselniky c, string? rzpDir)
{
@@ -40,9 +89,8 @@ public static class LogaResolver
private static void PridejZeSlozky(IDictionary d, string hladina, string? soubor)
{
- if (string.IsNullOrWhiteSpace(soubor)) return;
- var cesta = Path.IsPathRooted(soubor) ? soubor : Path.Combine(LogaDir, soubor);
- if (File.Exists(cesta)) d[hladina] = cesta;
+ var cesta = Najdi(soubor);
+ if (cesta is not null) d[hladina] = cesta;
}
/// Cesta ke schématu: absolutní beze změny, jinak relativně ke složce .rzp.
diff --git a/Rozpisky/ViewModels/MainViewModel.cs b/Rozpisky/ViewModels/MainViewModel.cs
index faa7abd..e020911 100644
--- a/Rozpisky/ViewModels/MainViewModel.cs
+++ b/Rozpisky/ViewModels/MainViewModel.cs
@@ -215,20 +215,14 @@ public sealed class MainViewModel : ObservableObject
public IReadOnlyDictionary ResolveLoga(Priloha r)
=> LogaResolver.Resolve(Projekt, r, Ciselniky, RzpSlozka);
- /// Jména DXF souborů ve složce loga\ vedle .exe (prázdná položka = „žádné") –
- /// nabídka pro výběr log financujících institucí v Údajích projektu.
+ /// Jména DXF log z obou složek loga\ – uživatelské i dodávané (prázdná položka
+ /// = „žádné") – nabídka pro výběr log financujících institucí v Údajích projektu.
public IReadOnlyList DostupnaLoga
{
get
{
var list = new List { "" };
- try
- {
- if (Directory.Exists(LogaResolver.LogaDir))
- list.AddRange(Directory.GetFiles(LogaResolver.LogaDir, "*.dxf")
- .Select(Path.GetFileName)!);
- }
- catch { /* nedostupná složka → jen prázdná položka */ }
+ list.AddRange(LogaResolver.DostupnaLoga());
return list;
}
}
@@ -311,6 +305,9 @@ public sealed class MainViewModel : ObservableObject
public ICommand ExportSeznamuCmd { get; }
public ICommand OtevritNapoveduCmd { get; }
public ICommand OtevritManualCmd { get; }
+
+ /// Otevře v Průzkumníku složku, kam si uživatel dává vlastní loga.
+ public ICommand OtevritSlozkuLogCmd { get; }
public ICommand OProgramuCmd { get; }
public MainViewModel()
@@ -407,6 +404,7 @@ public sealed class MainViewModel : ObservableObject
ExportSeznamuCmd = new RelayCommand(ExportujSeznamDoXlsx, () => Prilohy.Count > 0);
OtevritNapoveduCmd = new RelayCommand(OtevritNapovedu);
OtevritManualCmd = new RelayCommand(OtevritManual);
+ OtevritSlozkuLogCmd = new RelayCommand(OtevritSlozkuLog);
OProgramuCmd = new RelayCommand(OProgramu);
SledujZmeny();
@@ -1399,4 +1397,21 @@ public sealed class MainViewModel : ObservableObject
"Nápověda", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
+
+ /// Otevře v Průzkumníku složku pro uživatelská loga (a založí ji, když ještě není).
+ /// Vlastní loga patří sem, ne do složky programu – tu přeinstalace přepíše.
+ private void OtevritSlozkuLog()
+ {
+ var slozka = LogaResolver.UzivatelskaLogaDir;
+ try
+ {
+ Directory.CreateDirectory(slozka);
+ Process.Start(new ProcessStartInfo(slozka) { UseShellExecute = true });
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("Složku s logy se nepodařilo otevřít:\n" + slozka + "\n\n" + ex.Message,
+ "Loga", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
}
diff --git a/Rozpisky/Views/NastaveniView.xaml b/Rozpisky/Views/NastaveniView.xaml
index 480bc8e..d204cfb 100644
--- a/Rozpisky/Views/NastaveniView.xaml
+++ b/Rozpisky/Views/NastaveniView.xaml
@@ -1,4 +1,4 @@
-
+
@@ -39,6 +40,14 @@
+
+
+
+
+
+
diff --git a/Rozpisky/Views/UdajeProjektuView.xaml b/Rozpisky/Views/UdajeProjektuView.xaml
index c0d4d4a..3695ffd 100644
--- a/Rozpisky/Views/UdajeProjektuView.xaml
+++ b/Rozpisky/Views/UdajeProjektuView.xaml
@@ -1,4 +1,4 @@
-
+ Text="Loga dotačních programů (vlevo) a státních institucí (vpravo). DXF soubory dodávané s programem i vlastní (Nastavení → Otevřít složku s logy)."/>