Compare commits

...
4 Commits
Author SHA1 Message Date
marekandClaude Opus 5 ba52486f53 Dokumentace k rozvržení podkladů a vrstvám číselníků
ROZPISKY.md §2 a §8, ROZHRANI.md §3.4 (a odškrtnutá otevřená otázka v §9),
CLAUDE.md. Baseline testů v promptech subagentů 36 -> 50.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:50:23 +02:00
marekandClaude Opus 5 6cd41ee5a3 Číselníky ve dvou vrstvách: dodávaná a uživatelská
Dosud byl ciselniky.json jediný soubor vedle .exe, který si aplikace zároveň
dodávala i přepisovala. Přeinstalace novou verzí by uživateli přepsala
záznamy, které si sám nastřádal – ručně v záložce Číselníky i „samoučením“
jmen z projektů. Nově:

- dodávaná vrstva  ciselniky.vychozi.json vedle .exe, jen ke čtení; každá
  instalace ji obnoví, takže s novou verzí smí růst a měnit se,
- uživatelská vrstva  ciselniky.user.json v %APPDATA%\Rozpisky, kde je
  uložený jen rozdíl proti dodávané: Vlastni (přidané a upravené položky)
  a Skryte (klíče dodávaných položek, které si uživatel odklidil).

Slučování zachovává pořadí dodávaných položek, upravenou položku přepíše
na jejím místě a vlastní řadí za ně. Klíč se bere podle typu (Nazev /
Hodnota / Oznaceni), porovnává se bez ohledu na velikost písmen – stejně
jako to už dělá samoučení jmen. Obsah se porovnává serializací, takže
nová pole položek jsou pokrytá automaticky.

Migrace: chybí-li uživatelská vrstva a vedle .exe leží starý ciselniky.json,
vezme se jeho obsah jako aktuální stav, dopočítá se z něj rozdíl a originál
se přejmenuje na .migrovano. Nic se nemaže.

Backfill krajů v MainViewModelu tím ztratil smysl (kraje přicházejí
z dodávané vrstvy) a je odstraněný.

Slučování a dopočet rozdílu jsou zpřístupněné testům (InternalsVisibleTo) –
chyba v nich se projeví tichou ztrátou dat, takže je pokrývá 14 nových testů
včetně kolečka uložit→načíst a scénáře aktualizace programu.

Ověřeno i ručně: migrace 7,6 kB souboru na 708 B rozdílu, přežití smazání
celého bin/ a přebuildu, běh s nezapisovatelnou instalační složkou.

Build 0 chyb / 0 varování, 50 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:48:43 +02:00
marekandClaude Opus 5 3c0e30bf35 Uživatelská data do %APPDATA%\Rozpisky místo vedle .exe
Vše, co píše uživatel, míří přes novou třídu UzivatelskaData do profilu
uživatele: cad.json, layout.json, error.log, tisk_diag.log. Dosud se
zapisovalo vedle .exe, takže .exe v Program Files (složka jen pro čtení)
nebo přeinstalace novou verzí uživatele o data připravily.

Dodávané podklady (šablona, mustr, loga, mapovani.json) se dál čtou vedle
.exe – ty se naopak při každé instalaci obnovují. Hranice mezi tím, co se
instaluje, a tím, co patří uživateli, vede přesně tudy.

Portable režim zůstává: soubor portable.txt vedle .exe vrátí staré chování
(běh z USB, vývoj s jednou složkou).

Migrace: Migrace.PrenesStaraData() při startu přenese cad.json a layout.json
ze staré polohy, pokud v cíli ještě nejsou; originál označí příponou
.migrovano. Nic se nemaže a nic novějšího se nepřepíše.

Číselníky přijdou na řadu samostatně (rozpad na dodávanou a uživatelskou
vrstvu).

Build 0 chyb / 0 varování, 36 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:39:25 +02:00
marekandClaude Opus 5 d4eb3f2102 Restrukturalizace složky podle účelu podkladů
Kořen mísil tři nesouvisející věci. Nově:
- docs/            – podklady jen pro tvorbu programu (ROZPISKY, ROZHRANI,
                     PODKLAD_CAD, manuál SŽ v .md)
- Rozpisky/Podklady/ – podklady nutné k běhu, dodávané vedle .exe
                     (Rozpiska.dxf, SEZNAM.xlsx, manuál .pdf, ciselniky.json,
                     mapovani.json, loga/)
- vzorky/          – cizí vstupy pro ruční zkoušení (schema.dxf)

Mění se jen zdrojový strom. Rozvržení vedle .exe zůstává ploché (+ loga\)
přes metadata Link v .csproj, takže všechny resolvery nad AppContext.BaseDirectory
fungují beze změny. Konec odkazů přes ..\ ven z projektu.

Doprovodně: cesty v TestData.Logo, odkazy v dokumentaci, CLAUDE.md a v promptech
subagentů.

Build 0 chyb / 0 varování, 36 testů zelených.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:35:50 +02:00
36 changed files with 683 additions and 145 deletions
+6 -5
View File
@@ -1,4 +1,4 @@
--- ---
name: render-dxf 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). 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 tools: Read, Edit, Write, Grep, Glob, Bash, PowerShell
@@ -24,7 +24,7 @@ teče na obrazovku, do PNG, do PDF i do DXF. Nový výstup = **nová implementac
duplikovaný kreslicí kód. `DxfTemplate` jen čte DXF a přehrává ho do rozhraní — logika výstupu duplikovaný kreslicí kód. `DxfTemplate` jen čte DXF a přehrává ho do rozhraní — logika výstupu
do ní nepatří. do ní nepatří.
## Tvrdá pravidla DXF (z ROZPISKY.md, ověř si je tam) ## Tvrdá pravidla DXF (z docs/ROZPISKY.md, ověř si je tam)
- Tloušťka čáry = `Layer.LineWeight`, enum v 1/100 mm, **jen** povolené hodnoty: - Tloušťka čáry = `Layer.LineWeight`, enum v 1/100 mm, **jen** povolené hodnoty:
0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211. 0, 5, 9, 13, 15, 18, 20, 25, 30, 35, 40, 50, 53, 60, 70, 80, 90, 100, 106, 120, 140, 158, 200, 211.
@@ -40,15 +40,16 @@ do ní nepatří.
## Konvence ## Konvence
Kód, komentáře i názvy typů česky. `Nullable` je zapnutý. Nepřidávej NuGet balíčky. Kód, komentáře i názvy typů česky. `Nullable` je zapnutý. Nepřidávej NuGet balíčky.
Needituj produkční data: `Rozpiska.dxf`, `schema.dxf`, `Rozpisky/loga/*.dxf`. Needituj produkční data: `Rozpisky/Podklady/Rozpiska.dxf`, `vzorky/schema.dxf`,
`Rozpisky/Podklady/loga/*.dxf`.
## Postup ## Postup
1. Přečti si relevantní kapitolu `ROZPISKY.md` a dotčené soubory, než začneš editovat. 1. Přečti si relevantní kapitolu `docs/ROZPISKY.md` a dotčené soubory, než začneš editovat.
2. Změň kód co nejmenším zásahem, ve stylu okolního kódu. 2. Změň kód co nejmenším zásahem, ve stylu okolního kódu.
3. Ověř: 3. Ověř:
`dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a `dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
`dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` `dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q`
(baseline je 36 zelených testů — nesmí ubýt). (baseline je 50 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 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. neprošlo, řekni to rovnou i s chybovou hláškou — nezakrývej to.
+2 -2
View File
@@ -1,4 +1,4 @@
--- ---
name: verifikator 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. 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 tools: Read, Grep, Glob, Bash, PowerShell
@@ -17,7 +17,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 Testy spusť i tehdy, když build projde s varováními. Když build spadne, testy nespouštěj
a rovnou hlas chyby buildu. a rovnou hlas chyby buildu.
Baseline, se kterou porovnáváš: **build bez chyb a bez varování, 36 testů zelených, 0 přeskočených.** Baseline, se kterou porovnáváš: **build bez chyb a bez varování, 50 testů zelených, 0 přeskočených.**
Volitelně (jen když o to volající požádá) ověř, že aplikace naběhne: 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 spusť `Rozpisky\bin\Debug\net10.0-windows\Rozpisky.exe`, počkej ~8 s, zkontroluj, že proces
+3 -3
View File
@@ -1,4 +1,4 @@
--- ---
name: wpf-ui 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. 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 tools: Read, Edit, Write, Grep, Glob, Bash, PowerShell
@@ -31,11 +31,11 @@ nebo `Rozpisky/Xlsx/` jen popiš v odpovědi a nech je na volajícím.
## Postup ## Postup
1. `ROZHRANI.md` popisuje obrazovky, pole a datový model — přečti relevantní kapitolu, než 1. `docs/ROZHRANI.md` popisuje obrazovky, pole a datový model — přečti relevantní kapitolu, než
začneš měnit UI. začneš měnit UI.
2. `MainViewModel.cs` je velký; hledej v něm grepem, nečti ho celý zbytečně. 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 3. Ověř: `dotnet build "Rozpisky.sln" -nologo -v q -clp:ErrorsOnly` a
`dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` (baseline 36 zelených). `dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q` (baseline 50 zelených).
Build musí být **bez varování** — chyby v XAML se často projeví jen jako varování. 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ě 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. a doporuč ruční spuštění aplikace.
+28 -9
View File
@@ -1,4 +1,4 @@
# Rozpisky — pokyny pro Claude Code # Rozpisky — pokyny pro Claude Code
WPF desktopová aplikace (.NET 10, `net10.0-windows`, AnyCPU) — generátor výkresových rohových WPF desktopová aplikace (.NET 10, `net10.0-windows`, AnyCPU) — generátor výkresových rohových
razítek („rozpisek") pro Správu železnic. Náhrada původního excelového nástroje „Rozpisky 1.0.8". razítek („rozpisek") pro Správu železnic. Náhrada původního excelového nástroje „Rozpisky 1.0.8".
@@ -11,7 +11,7 @@ dotnet test "Rozpisky.Tests\Rozpisky.Tests.csproj" -nologo -v q
dotnet run --project "Rozpisky\Rozpisky.csproj" dotnet run --project "Rozpisky\Rozpisky.csproj"
``` ```
Baseline: build bez chyb a varování, **36 testů zelených**. Před hlášením hotovo obojí spustit Baseline: build bez chyb a varování, **50 testů zelených**. Před hlášením hotovo obojí spustit
(nebo použít `/overit`). (nebo použít `/overit`).
## Architektura — jediné pravidlo, které se nesmí porušit ## Architektura — jediné pravidlo, které se nesmí porušit
@@ -21,7 +21,7 @@ Tatáž kresba teče na obrazovku, do PNG, do PDF i do DXF. **Nový výstupní f
`IProfileRenderer`, nikdy nový kreslicí kód.** Čtečka [`DxfTemplate`](Rozpisky/Dxf/DxfTemplate.cs) `IProfileRenderer`, nikdy nový kreslicí kód.** Čtečka [`DxfTemplate`](Rozpisky/Dxf/DxfTemplate.cs)
jen čte DXF šablonu a „přehrává" ji do tohoto rozhraní. jen čte DXF šablonu a „přehrává" ji do tohoto rozhraní.
Podrobnosti (vrstvy, NuGet API, pravidla zápisu DXF) — [`ROZPISKY.md`](ROZPISKY.md). Nečíst celý, Podrobnosti (vrstvy, NuGet API, pravidla zápisu DXF) — [`ROZPISKY.md`](docs/ROZPISKY.md). Nečíst celý,
jen relevantní kapitolu. jen relevantní kapitolu.
## Konvence ## Konvence
@@ -35,13 +35,32 @@ jen relevantní kapitolu.
(WPF-DarkTheme-Kit). Žádné hardcoded `#RRGGBB` ani natvrdo zadané odsazení. (WPF-DarkTheme-Kit). Žádné hardcoded `#RRGGBB` ani natvrdo zadané odsazení.
- `Nullable` a `ImplicitUsings` jsou zapnuté v obou projektech. - `Nullable` a `ImplicitUsings` jsou zapnuté v obou projektech.
- COM (CAD, MS Excel) jde přes pozdní vazbu (`dynamic`) — nepřidávat interop assembly. - COM (CAD, MS Excel) jde přes pozdní vazbu (`dynamic`) — nepřidávat interop assembly.
- 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.
## Rozvržení složky — tři druhy podkladů
| Kde | Co to je |
|---|---|
| `docs/` | Podklady **jen pro tvorbu programu** — architektura, datový model, normativní manuál SŽ v `.md`. Nedodává se uživateli. |
| `Rozpisky/Podklady/` | Podklady **nutné k běhu** — kopírují se vedle `.exe` (plochá struktura + `loga\`, řídí `Link` v `.csproj`). Aplikace je **jen čte**. |
| `%APPDATA%\Rozpisky` | Podklady **vznikající za běhu** — vše, co píše uživatel. V repu nejsou a nemají tam co dělat. |
| `vzorky/` | Cizí vstupy pro ruční zkoušení (`schema.dxf`). Nedodává se. |
Nové dodávané datum = do `Rozpisky/Podklady/` **a** řádek v `.csproj` s `Link`, ať výstup
zůstane plochý. Nový zapisovaný soubor = přes `UzivatelskaData.Cesta(...)`, nikdy
`AppContext.BaseDirectory`.
## Kam nesahat ## Kam nesahat
- `Rozpiska.dxf`, `schema.dxf` — produkční DXF šablony. - `Rozpisky/Podklady/Rozpiska.dxf`, `vzorky/schema.dxf` — produkční DXF šablony.
- `Rozpisky/loga/*.dxf` — dodávaná loga (9 ks, testy je renderují). - `Rozpisky/Podklady/loga/*.dxf` — dodávaná loga (9 ks, testy je renderují).
- `SEZNAM.xlsx` — mustr „Seznam příloh". - `Rozpisky/Podklady/SEZNAM.xlsx` — mustr „Seznam příloh".
- `SZ_SM011_P10_Manual_v6.md/.pdf` — normativní podklad SŽ, jen ke čtení. - `docs/SZ_SM011_P10_Manual_v6.md`, `Rozpisky/Podklady/SZ_SM011_P10_Manual_v6.pdf`
— normativní podklad SŽ, jen ke čtení.
## Delegování ## Delegování
@@ -51,5 +70,5 @@ celý do jedné z těchto domén je použít; drobnosti napříč vrstvami řeš
## Další dokumentace ## Další dokumentace
[`ROZHRANI.md`](ROZHRANI.md) — obrazovky, pole a datový model. [`ROZHRANI.md`](docs/ROZHRANI.md) — obrazovky, pole a datový model.
[`PODKLAD_CAD_Layout_Tisk.md`](PODKLAD_CAD_Layout_Tisk.md) — CAD, layouty a tisk přes COM. [`PODKLAD_CAD_Layout_Tisk.md`](docs/PODKLAD_CAD_Layout_Tisk.md) — CAD, layouty a tisk přes COM.
+191
View File
@@ -0,0 +1,191 @@
using System.Collections.ObjectModel;
using Rozpisky.Data;
using Rozpisky.Models;
using Xunit;
namespace Rozpisky.Tests;
/// <summary>
/// Slučování dodávané a uživatelské vrstvy číselníků a dopočet rozdílu
/// (<see cref="CiselnikyStore.Sluc{T}"/> / <see cref="CiselnikyStore.Rozdil{T}"/>).
/// Na téhle logice stojí, jestli uživatel po aktualizaci programu najde svoje záznamy
/// chyba tady se projeví tichou ztrátou dat, kterou nikdo nenahlásí.
/// </summary>
public class CiselnikyVrstvyTesty
{
private static List<Jmeno> Vychozi(params string[] jmena)
=> jmena.Select(j => new Jmeno { Hodnota = j }).ToList();
private static ObservableCollection<Jmeno> Sluc(
List<Jmeno> vychozi, CiselnikyStore.Vrstva<Jmeno>? uziv)
{
var cil = new ObservableCollection<Jmeno>();
CiselnikyStore.Sluc(cil, vychozi, uziv);
return cil;
}
[Fact]
public void BezUzivatelskeVrstvyZustaneDodavanaBezeZmeny()
{
var vysledek = Sluc(Vychozi("Novák", "Svoboda"), null);
Assert.Equal(new[] { "Novák", "Svoboda" }, vysledek.Select(j => j.Hodnota));
}
[Fact]
public void VlastniPolozkySeRadiZaDodavane()
{
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Vlastni = Vychozi("Dvořák") };
var vysledek = Sluc(Vychozi("Novák", "Svoboda"), uziv);
Assert.Equal(new[] { "Novák", "Svoboda", "Dvořák" }, vysledek.Select(j => j.Hodnota));
}
[Fact]
public void SkrytaDodavanaPolozkaSeNeobjevi()
{
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Skryte = { "Svoboda" } };
var vysledek = Sluc(Vychozi("Novák", "Svoboda"), uziv);
Assert.Equal(new[] { "Novák" }, vysledek.Select(j => j.Hodnota));
}
[Fact]
public void UpravenaDodavanaPolozkaSePrepiseNaSvemMiste()
{
// Uživatel opravil adresu dodávané organizace musí zůstat na původní pozici,
// jinak by se mu po každé aktualizaci přeházelo pořadí v našeptávači.
var vychozi = new List<Organizace>
{
new() { Nazev = "SŽ Praha", Adresa = "stará" },
new() { Nazev = "SŽ Brno", Adresa = "Kounicova 26" },
};
var uziv = new CiselnikyStore.Vrstva<Organizace>
{
Vlastni = { new Organizace { Nazev = "SŽ Praha", Adresa = "Dlážděná 1003/7" } },
};
var cil = new ObservableCollection<Organizace>();
CiselnikyStore.Sluc(cil, vychozi, uziv);
Assert.Equal(new[] { "SŽ Praha", "SŽ Brno" }, cil.Select(o => o.Nazev));
Assert.Equal("Dlážděná 1003/7", cil[0].Adresa);
}
[Fact]
public void ParovaniIgnorujeVelikostPismen()
{
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Vlastni = Vychozi("NOVÁK") };
var vysledek = Sluc(Vychozi("Novák"), uziv);
Assert.Single(vysledek);
Assert.Equal("NOVÁK", vysledek[0].Hodnota);
}
[Fact]
public void RozepsaneRadkySeNeslijouDoJednoho()
{
// Dva prázdné řádky přidané tlačítkem „Přidat záznam“ mají stejný (prázdný) klíč
// nesmí se spárovat, jinak by uživateli jeden zmizel dřív, než ho stihne vyplnit.
var uziv = new CiselnikyStore.Vrstva<Jmeno> { Vlastni = Vychozi("", "") };
var vysledek = Sluc(Vychozi("Novák"), uziv);
Assert.Equal(3, vysledek.Count);
}
[Fact]
public void RozdilZachytiPridaniUpravuISkryti()
{
var vychozi = Vychozi("Novák", "Svoboda", "Černý");
// Uživatel: Svobodu smazal, přidal Dvořáka, Novák zůstal beze změny.
var stav = Vychozi("Novák", "Černý", "Dvořák");
var rozdil = CiselnikyStore.Rozdil(stav, vychozi);
Assert.Equal(new[] { "Dvořák" }, rozdil.Vlastni.Select(j => j.Hodnota));
Assert.Equal(new[] { "Svoboda" }, rozdil.Skryte);
}
[Fact]
public void RozdilNepovazujeNezmenenouPolozkuZaVlastni()
{
var vychozi = Vychozi("Novák", "Svoboda");
var rozdil = CiselnikyStore.Rozdil(Vychozi("Novák", "Svoboda"), vychozi);
Assert.Empty(rozdil.Vlastni);
Assert.Empty(rozdil.Skryte);
}
[Fact]
public void RozdilZachytiZmenuJinehoNezKlicovehoPole()
{
var vychozi = new List<Firma>
{
new() { Nazev = "EXprojekt s.r.o.", Telefon = "+420 533 312 000" },
};
var stav = new List<Firma>
{
new() { Nazev = "EXprojekt s.r.o.", Telefon = "+420 111 222 333" },
};
var rozdil = CiselnikyStore.Rozdil(stav, vychozi);
Assert.Single(rozdil.Vlastni);
Assert.Equal("+420 111 222 333", rozdil.Vlastni[0].Telefon);
Assert.Empty(rozdil.Skryte); // klíč zůstal, nejde o skrytí
}
[Fact]
public void PrejmenovaniDodavanePolozkySeUloziJakoSkrytiAPridani()
{
var vychozi = Vychozi("Nowak");
var rozdil = CiselnikyStore.Rozdil(Vychozi("Novák"), vychozi);
Assert.Equal(new[] { "Novák" }, rozdil.Vlastni.Select(j => j.Hodnota));
Assert.Equal(new[] { "Nowak" }, rozdil.Skryte);
}
[Fact]
public void RozdilASlouceniDavajiZpetPuvodniStav()
{
// Kolečko uložit → načíst nesmí nic ztratit ani přidat, ať se s číselníkem stalo cokoli.
var vychozi = Vychozi("Novák", "Svoboda", "Černý");
var stav = Vychozi("Novák", "Černý", "Dvořák", "Horák");
var vysledek = Sluc(vychozi, CiselnikyStore.Rozdil(stav, vychozi));
Assert.Equal(stav.Select(j => j.Hodnota), vysledek.Select(j => j.Hodnota));
}
[Fact]
public void NovaDodavanaPolozkaSePoAktualizaciObjeviAVlastniZustanou()
{
// Jádro celé změny: program vydá novou verzi s doplněným číselníkem, uživatelův
// rozdíl je pořád ten starý nová položka musí přibýt, vlastní nesmí zmizet.
var stareVychozi = Vychozi("Novák");
var rozdilUzivatele = CiselnikyStore.Rozdil(Vychozi("Novák", "Dvořák"), stareVychozi);
var noveVychozi = Vychozi("Novák", "Svoboda");
var vysledek = Sluc(noveVychozi, rozdilUzivatele);
Assert.Equal(new[] { "Novák", "Svoboda", "Dvořák" }, vysledek.Select(j => j.Hodnota));
}
[Fact]
public void SkrytiPrezijeAktualizaciDodavaneVrstvy()
{
var stareVychozi = Vychozi("Novák", "Svoboda");
var rozdilUzivatele = CiselnikyStore.Rozdil(Vychozi("Novák"), stareVychozi);
// Nová verze programu Svobodu pořád dodává uživatel ho ale jednou odklidil.
var vysledek = Sluc(Vychozi("Novák", "Svoboda", "Černý"), rozdilUzivatele);
Assert.Equal(new[] { "Novák", "Černý" }, vysledek.Select(j => j.Hodnota));
}
[Fact]
public void KlicSeBerePodleTypuPolozky()
{
Assert.Equal("SŽ", CiselnikyStore.Klic(new Organizace { Nazev = "SŽ" }));
Assert.Equal("EXprojekt", CiselnikyStore.Klic(new Firma { Nazev = "EXprojekt" }));
Assert.Equal("Novák", CiselnikyStore.Klic(new Jmeno { Hodnota = "Novák" }));
Assert.Equal("DPS", CiselnikyStore.Klic(new VerzeDokumentace { Hodnota = "DPS" }));
Assert.Equal("D", CiselnikyStore.Klic(new Cast { Oznaceni = "D", Nazev = "Mosty" }));
Assert.Equal("Jihomoravský", CiselnikyStore.Klic(new Kraj { Nazev = "Jihomoravský" }));
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
using System.IO; using System.IO;
namespace Rozpisky.Tests; namespace Rozpisky.Tests;
@@ -8,8 +8,8 @@ public static class TestData
/// <summary>Kořen repozitáře nalezen od bin složky testů směrem nahoru podle Rozpisky.sln.</summary> /// <summary>Kořen repozitáře nalezen od bin složky testů směrem nahoru podle Rozpisky.sln.</summary>
public static string RepoRoot { get; } = NajdiKoren(); public static string RepoRoot { get; } = NajdiKoren();
/// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\loga</c>.</summary> /// <summary>Plná cesta k logu ve zdrojové složce <c>Rozpisky\Podklady\loga</c>.</summary>
public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "loga", fileName); public static string Logo(string fileName) => Path.Combine(RepoRoot, "Rozpisky", "Podklady", "loga", fileName);
/// <summary>Všech 9 log dodávaných s programem.</summary> /// <summary>Všech 9 log dodávaných s programem.</summary>
public static readonly string[] VsechnaLoga = public static readonly string[] VsechnaLoga =
+11 -6
View File
@@ -1,4 +1,4 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Windows; using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
@@ -7,8 +7,9 @@ namespace Rozpisky
{ {
/// <summary> /// <summary>
/// Globální záchytná síť pro neošetřené výjimky aplikace nesmí spadnout bez stopy. /// Globální záchytná síť pro neošetřené výjimky aplikace nesmí spadnout bez stopy.
/// Každá výjimka se zapíše do <c>error.log</c> vedle .exe (stejný portable princip jako /// Každá výjimka se zapíše do <c>error.log</c> v uživatelské složce
/// <c>cad.json</c>/<c>layout.json</c>) a uživateli se ukáže MessageBox. Výjimky na UI /// (<see cref="Data.UzivatelskaData"/>, stejně jako <c>cad.json</c>/<c>layout.json</c>)
/// a uživateli se ukáže MessageBox. Výjimky na UI
/// (Dispatcher) vlákně se označí jako zpracované, takže aplikace běží dál rozdělaný /// (Dispatcher) vlákně se označí jako zpracované, takže aplikace běží dál rozdělaný
/// projekt zůstane otevřený a jde uložit. /// projekt zůstane otevřený a jde uložit.
/// </summary> /// </summary>
@@ -18,6 +19,10 @@ namespace Rozpisky
{ {
base.OnStartup(e); base.OnStartup(e);
// Data starších verzí ležela vedle .exe přenést do uživatelské složky dřív,
// než si na ně sáhne kterýkoli store (ten už čte jen nové umístění).
Data.Migrace.PrenesStaraData();
// Úklid diagnostického logu tisku, ať po měsících nenaroste do enormních rozměrů. // Úklid diagnostického logu tisku, ať po měsících nenaroste do enormních rozměrů.
Cad.CadTiskService.UklidLog(); Cad.CadTiskService.UklidLog();
@@ -46,14 +51,14 @@ namespace Rozpisky
{ {
string radek = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {zdroj}: {ex?.ToString() ?? "(neznámá výjimka)"}" string radek = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {zdroj}: {ex?.ToString() ?? "(neznámá výjimka)"}"
+ Environment.NewLine + Environment.NewLine; + Environment.NewLine + Environment.NewLine;
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"), radek, Encoding.UTF8); File.AppendAllText(Data.UzivatelskaData.Cesta("error.log"), radek, Encoding.UTF8);
} }
catch { /* read-only složka apod. MessageBox se ukáže i tak */ } catch { /* nezapisovatelná složka apod. MessageBox se ukáže i tak */ }
} }
private static void UkazChybu(Exception ex) => MessageBox.Show( private static void UkazChybu(Exception ex) => MessageBox.Show(
"Nastala neočekávaná chyba, aplikace ale běží dál doporučujeme uložit projekt.\n\n" + "Nastala neočekávaná chyba, aplikace ale běží dál doporučujeme uložit projekt.\n\n" +
$"{ex.Message}\n\nPodrobnosti jsou v souboru error.log vedle aplikace.", $"{ex.Message}\n\nPodrobnosti jsou v souboru error.log ve složce\n{Data.UzivatelskaData.Slozka}",
"Neočekávaná chyba", MessageBoxButton.OK, MessageBoxImage.Error); "Neočekávaná chyba", MessageBoxButton.OK, MessageBoxImage.Error);
} }
} }
+6 -6
View File
@@ -1,4 +1,4 @@
using System.IO; using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using Rozpisky.Data; using Rozpisky.Data;
@@ -7,9 +7,9 @@ namespace Rozpisky.Cad;
/// <summary> /// <summary>
/// Načítání/ukládání seznamu podporovaných CAD systémů + naposledy vybraného enginu do JSON souboru /// Načítání/ukládání seznamu podporovaných CAD systémů + naposledy vybraného enginu do JSON souboru
/// <b>vedle .exe</b> (portable). Stejný princip jako <see cref="Data.CiselnikyStore"/> / /// v <b>uživatelské složce</b> (<see cref="Data.UzivatelskaData"/>). CAD je vlastnost stroje
/// <see cref="Dxf.MapovaniStore"/>: CAD je vlastnost stroje (ne zakázky), takže výběr žije mimo /// (ne zakázky), takže výběr žije mimo projekt a je společný všem projektům a mimo instalační
/// projekt a je globální pro celou instalaci. /// složku, aby o něj uživatel při přeinstalaci nepřišel.
/// </summary> /// </summary>
public static class CadEnginesStore public static class CadEnginesStore
{ {
@@ -21,8 +21,8 @@ public static class CadEnginesStore
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
}; };
/// <summary>Výchozí umístění: soubor vedle .exe (portable).</summary> /// <summary>Výchozí umístění: uživatelská složka (viz <see cref="UzivatelskaData"/>).</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev); public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev);
/// <summary>Uložený výběr CADu: buď předdefinovaný engine (<see cref="VybranyProgId"/>), nebo /// <summary>Uložený výběr CADu: buď předdefinovaný engine (<see cref="VybranyProgId"/>), nebo
/// ruční režim „Manuálně" (<see cref="RucniRezim"/> + <see cref="RucniProgId"/>). Režim a ruční /// ruční režim „Manuálně" (<see cref="RucniRezim"/> + <see cref="RucniProgId"/>). Režim a ruční
+5 -4
View File
@@ -1,4 +1,4 @@
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -196,14 +196,15 @@ public static class CadTiskService
{ {
try try
{ {
var cesta = Path.Combine(AppContext.BaseDirectory, "tisk_diag.log"); var cesta = Data.UzivatelskaData.Cesta("tisk_diag.log");
if (File.Exists(cesta) && new FileInfo(cesta).Length > MaxLogBajtu) if (File.Exists(cesta) && new FileInfo(cesta).Length > MaxLogBajtu)
File.Delete(cesta); File.Delete(cesta);
} }
catch { /* read-only složka apod. úklid nesmí přerušit start */ } catch { /* read-only složka apod. úklid nesmí přerušit start */ }
} }
/// <summary>Diagnostický „chlebníček“ zapíše krok tisku do <c>tisk_diag.log</c> vedle .exe. /// <summary>Diagnostický „chlebníček“ zapíše krok tisku do <c>tisk_diag.log</c>
/// v uživatelské složce.
/// Slouží k lokalizaci tvrdého pádu CADu (nativní chyba v <c>PlotToFile</c>), který nezanechá /// Slouží k lokalizaci tvrdého pádu CADu (nativní chyba v <c>PlotToFile</c>), který nezanechá
/// stopu v error.log ani v log okně (obojí zmizí s procesem). Zapisuje se synchronně (flush ihned), /// stopu v error.log ani v log okně (obojí zmizí s procesem). Zapisuje se synchronně (flush ihned),
/// takže poslední řádek přežije i pád procesu. Diagnostika sama nesmí nic shodit.</summary> /// takže poslední řádek přežije i pád procesu. Diagnostika sama nesmí nic shodit.</summary>
@@ -211,7 +212,7 @@ public static class CadTiskService
{ {
try try
{ {
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "tisk_diag.log"), File.AppendAllText(Data.UzivatelskaData.Cesta("tisk_diag.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {krok}{Environment.NewLine}"); $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {krok}{Environment.NewLine}");
} }
catch { /* read-only složka apod. nesmí přerušit tisk */ } catch { /* read-only složka apod. nesmí přerušit tisk */ }
+5 -5
View File
@@ -1,4 +1,4 @@
using System.IO; using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
using Rozpisky.Data; using Rozpisky.Data;
@@ -6,8 +6,8 @@ using Rozpisky.Data;
namespace Rozpisky.Cad; namespace Rozpisky.Cad;
/// <summary> /// <summary>
/// Načítání/ukládání <see cref="LayoutConfig"/> do JSON souboru <b>vedle .exe</b> (portable). Stejný /// Načítání/ukládání <see cref="LayoutConfig"/> do JSON souboru v <b>uživatelské složce</b>
/// princip jako <see cref="CadEnginesStore"/> konfigurace layoutů/tisku je vlastnost stroje/instalace /// (<see cref="Data.UzivatelskaData"/>). Stejný princip jako <see cref="CadEnginesStore"/> konfigurace layoutů/tisku je vlastnost stroje/instalace
/// (bloky a hladiny šablony, dostupné tiskárny), needituje se přes UI, jen ručně v souboru. /// (bloky a hladiny šablony, dostupné tiskárny), needituje se přes UI, jen ručně v souboru.
/// </summary> /// </summary>
public static class LayoutConfigStore public static class LayoutConfigStore
@@ -20,8 +20,8 @@ public static class LayoutConfigStore
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
}; };
/// <summary>Výchozí umístění: soubor vedle .exe (portable).</summary> /// <summary>Výchozí umístění: uživatelská složka (viz <see cref="UzivatelskaData"/>).</summary>
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev); public static string DefaultPath => UzivatelskaData.Cesta(SouborNazev);
/// <summary>Načte konfiguraci. Když soubor chybí/je poškozený, vrátí vestavěné výchozí hodnoty /// <summary>Načte konfiguraci. Když soubor chybí/je poškozený, vrátí vestavěné výchozí hodnoty
/// (a rovnou je uloží, aby šel soubor dál ručně upravovat).</summary> /// (a rovnou je uloží, aby šel soubor dál ručně upravovat).</summary>
+224 -41
View File
@@ -1,4 +1,4 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Text.Json; using System.Text.Json;
@@ -7,64 +7,213 @@ using Rozpisky.Models;
namespace Rozpisky.Data; namespace Rozpisky.Data;
/// <summary> /// <summary>
/// Načítání/ukládání sdílených číselníků (organizace, firmy, jména, verze, části) /// Sdílené číselníky (organizace, firmy, jména, verze, části, kraje) ve <b>dvou vrstvách</b>:
/// do JSON souboru <b>vedle .exe</b> (portable). Číselníky jsou společné všem projektům /// <list type="bullet">
/// (na rozdíl od <c>.rzp</c> přes <see cref="ProjektStore"/>), takže žijí mimo projekt. /// <item><b>dodávaná</b> <c>ciselniky.vychozi.json</c> vedle .exe, jen ke čtení; každá instalace
/// Bez NuGet závislostí System.Text.Json je součást .NET. /// ji obnoví, takže smí s novou verzí programu růst a měnit se;</item>
/// <item><b>uživatelská</b> <c>ciselniky.user.json</c> v uživatelské složce
/// (<see cref="UzivatelskaData"/>), kde je uložený jen <b>rozdíl</b> proti dodávané vrstvě.</item>
/// </list>
/// Důvod rozdělení: číselníky uživatel průběžně doplňuje (ručně i „samoučením“ jmen z projektů).
/// Kdyby žily v jednom souboru vedle .exe, přeinstalace novou verzí by je přepsala a uživatel by
/// o svoje záznamy přišel. Uložením rozdílu se obojí smíří uživatel si nese své, program své.
/// <para>Bez NuGet závislostí System.Text.Json je součást .NET.</para>
/// </summary> /// </summary>
public static class CiselnikyStore public static class CiselnikyStore
{ {
public const string SouborNazev = "ciselniky.json"; /// <summary>Dodávaná vrstva vedle .exe (jen ke čtení).</summary>
public const string VychoziNazev = "ciselniky.vychozi.json";
/// <summary>Uživatelská vrstva (rozdíl proti dodávané) v uživatelské složce.</summary>
public const string UzivatelskyNazev = "ciselniky.user.json";
/// <summary>Jednovrstvý soubor starších verzí vedle .exe zdroj pro jednorázovou migraci.</summary>
public const string StaryNazev = "ciselniky.json";
private static readonly JsonSerializerOptions Opts = new() private static readonly JsonSerializerOptions Opts = new()
{ {
WriteIndented = true, WriteIndented = true,
// Diakritika čitelně (ě š č…), ne jako \uXXXX. // Diakritika čitelně (ě š č…), ne jako escape sekvence.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
}; };
/// <summary>Výchozí umístění: soubor vedle .exe (portable). Lze přepsat parametrem cesty.</summary> public static string VychoziPath => Path.Combine(AppContext.BaseDirectory, VychoziNazev);
public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev); public static string UzivatelskyPath => UzivatelskaData.Cesta(UzivatelskyNazev);
public static string StaryPath => Path.Combine(AppContext.BaseDirectory, StaryNazev);
/// <summary>Naplní cílové číselníky obsahem z JSON. Vrací false, když soubor neexistuje /// <summary>Dodávaná vrstva načtená při startu. Drží se po celý běh, protože je referenčním
/// nebo je poškozený volající pak nasází výchozí obsah (a uloží nový soubor).</summary> /// bodem pro dopočet rozdílu při každém uložení kdyby se načítala znovu, změna souboru
public static bool LoadInto(Ciselniky target, string? path = null) /// za běhu by rozdíl rozhodila.</summary>
private static Dto? _vychozi;
/// <summary>Načte číselníky: dodávaná vrstva + rozdíl uživatele. Když dodávaná vrstva chybí
/// (poškozená instalace), použije se vestavěný <see cref="DemoData.SeedCiselniky"/>, ať má
/// uživatel s čím pracovat. Před prvním načtením proběhne migrace ze starého jednovrstvého
/// souboru.</summary>
public static void Nacti(Ciselniky cil)
{ {
path ??= DefaultPath; _vychozi = NactiVychozi();
if (!File.Exists(path)) return false; MigrujStarySoubor();
var uziv = NactiUzivatelskou();
Sluc(cil.Organizace, _vychozi.Organizace, uziv?.Organizace);
Sluc(cil.Firmy, _vychozi.Firmy, uziv?.Firmy);
Sluc(cil.Jmena, _vychozi.Jmena, uziv?.Jmena);
Sluc(cil.VerzeDokumentace, _vychozi.VerzeDokumentace, uziv?.VerzeDokumentace);
Sluc(cil.Casti, _vychozi.Casti, uziv?.Casti);
Sluc(cil.Kraje, _vychozi.Kraje, uziv?.Kraje);
}
/// <summary>Uloží <b>rozdíl</b> aktuálního stavu proti dodávané vrstvě. Chyby zápisu propustí
/// volajícímu, ať je ošetří dle kontextu (autosave je spolkne, ruční uložení zahlásí).</summary>
public static void UlozRozdil(Ciselniky c)
{
var v = _vychozi ??= NactiVychozi();
var dto = new UzivatelskyDto
{
Organizace = Rozdil(c.Organizace, v.Organizace),
Firmy = Rozdil(c.Firmy, v.Firmy),
Jmena = Rozdil(c.Jmena, v.Jmena),
VerzeDokumentace = Rozdil(c.VerzeDokumentace, v.VerzeDokumentace),
Casti = Rozdil(c.Casti, v.Casti),
Kraje = Rozdil(c.Kraje, v.Kraje),
};
JsonConfigFile.ZapisAtomicky(UzivatelskyPath, JsonSerializer.Serialize(dto, Opts));
}
// ---- Vrstvy ----
private static Dto NactiVychozi()
{
if (File.Exists(VychoziPath))
{
try
{
var data = JsonSerializer.Deserialize<Dto>(File.ReadAllText(VychoziPath), Opts);
if (data is not null) return data;
JsonConfigFile.ZalohujAOhlasPoskozeny(VychoziPath);
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
JsonConfigFile.ZalohujAOhlasPoskozeny(VychoziPath, ex);
}
}
// Chybějící/poškozená dodávaná vrstva se nezapisuje zpátky (je součástí instalace)
// jen se nahradí vestavěným obsahem, ať aplikace není bez číselníků.
var nahrada = new Ciselniky();
DemoData.SeedCiselniky(nahrada);
return Dto.Z(nahrada);
}
private static UzivatelskyDto? NactiUzivatelskou()
{
if (!File.Exists(UzivatelskyPath)) return null;
try try
{ {
// Čteme do DTO se settable List<T>: System.Text.Json neplní get-only ObservableCollection. return JsonSerializer.Deserialize<UzivatelskyDto>(File.ReadAllText(UzivatelskyPath), Opts);
var data = JsonSerializer.Deserialize<Dto>(File.ReadAllText(path), Opts);
if (data is null)
{
// Soubor existuje, ale nenese data („null") → stejné zacházení jako s rozbitým JSON.
JsonConfigFile.ZalohujAOhlasPoskozeny(path);
return false;
}
Replace(target.Organizace, data.Organizace);
Replace(target.Firmy, data.Firmy);
Replace(target.Jmena, data.Jmena);
Replace(target.VerzeDokumentace, data.VerzeDokumentace);
Replace(target.Casti, data.Casti);
Replace(target.Kraje, data.Kraje);
return true;
} }
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException) catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{ {
// Poškozený / nečitelný soubor → fallback na seed. Volající pak soubor přepíše výchozím // Ruční editace s překlepem nesmí uživatelovy záznamy tiše zahodit zazálohovat a ohlásit.
// obsahem, proto se originál nejdřív zazálohuje a uživatel dozví (jinak by o své JsonConfigFile.ZalohujAOhlasPoskozeny(UzivatelskyPath, ex);
// číselníky přišel tiše). return null;
JsonConfigFile.ZalohujAOhlasPoskozeny(path, ex);
return false;
} }
} }
/// <summary>Uloží číselníky do JSON (atomicky přes .tmp pád uprostřed zápisu nesmí soubor /// <summary>Jednorázový převod ze starého jednovrstvého <c>ciselniky.json</c> vedle .exe:
/// poškodit). Chyby zápisu (např. read-only složka u .exe v Program Files) propustí volajícímu, /// jeho obsah je „aktuální stav“ uživatele, takže se z něj dopočítá rozdíl proti dodávané
/// ať je může ošetřit dle kontextu (autosave je spolkne, ruční uložení zahlásí).</summary> /// vrstvě a uloží jako uživatelská vrstva. Originál se jen přejmenuje, nikdy nemaže.</summary>
public static void Save(Ciselniky c, string? path = null) private static void MigrujStarySoubor()
=> JsonConfigFile.ZapisAtomicky(path ?? DefaultPath, JsonSerializer.Serialize(c, Opts)); {
if (File.Exists(UzivatelskyPath) || !File.Exists(StaryPath)) return;
try
{
var stary = JsonSerializer.Deserialize<Dto>(File.ReadAllText(StaryPath), Opts);
if (stary is null) return;
var stav = new Ciselniky();
Replace(stav.Organizace, stary.Organizace);
Replace(stav.Firmy, stary.Firmy);
Replace(stav.Jmena, stary.Jmena);
Replace(stav.VerzeDokumentace, stary.VerzeDokumentace);
Replace(stav.Casti, stary.Casti);
Replace(stav.Kraje, stary.Kraje);
UlozRozdil(stav);
Migrace.OznacJakoPrenesene(StaryPath);
}
catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException)
{
// Nepovedená migrace nesmí zabránit startu; starý soubor zůstane a zkusí se příště.
}
}
// ---- Slučování a rozdíl ----
/// <summary>Klíč položky podle něj se poznává „tatáž“ položka napříč vrstvami.
/// Porovnává se bez ohledu na velikost písmen, stejně jako to dělá samoučení jmen.</summary>
internal static string Klic(object polozka) => polozka switch
{
Organizace o => o.Nazev,
Firma f => f.Nazev,
Jmeno j => j.Hodnota,
VerzeDokumentace v => v.Hodnota,
Cast c => c.Oznaceni,
Kraj k => k.Nazev,
_ => "",
};
/// <summary>Dodávaná vrstva → odebrat skryté → přepsat/doplnit vlastními. Pořadí dodávaných
/// položek zůstane zachované, vlastní se řadí za ně.</summary>
internal static void Sluc<T>(ObservableCollection<T> cil, List<T> vychozi, Vrstva<T>? uziv)
{
var skryte = new HashSet<string>(uziv?.Skryte ?? new List<string>(), StringComparer.OrdinalIgnoreCase);
var vysledek = vychozi.Where(x => !skryte.Contains(Klic(x!))).ToList();
foreach (var vlastni in uziv?.Vlastni ?? new List<T>())
{
var klic = Klic(vlastni!);
// Prázdný klíč = rozepsaný řádek; ten se nikdy nespáruje, jen připojí (jinak by se
// dva rozepsané řádky slily do jednoho).
int i = string.IsNullOrEmpty(klic)
? -1
: vysledek.FindIndex(x => string.Equals(Klic(x!), klic, StringComparison.OrdinalIgnoreCase));
if (i >= 0) vysledek[i] = vlastni; else vysledek.Add(vlastni);
}
Replace(cil, vysledek);
}
/// <summary>Rozdíl aktuálního stavu proti dodávané vrstvě: co v ní není nebo se od ní liší,
/// jde do <c>Vlastni</c>; dodávaná položka, která ve stavu chybí, jde do <c>Skryte</c>.</summary>
internal static Vrstva<T> Rozdil<T>(IEnumerable<T> aktualni, List<T> vychozi)
{
var stav = aktualni.ToList();
var vychoziMapa = vychozi
.GroupBy(x => Klic(x!), StringComparer.OrdinalIgnoreCase)
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
var klicStavu = new HashSet<string>(stav.Select(x => Klic(x!)), StringComparer.OrdinalIgnoreCase);
var vrstva = new Vrstva<T>();
foreach (var polozka in stav)
{
var klic = Klic(polozka!);
if (string.IsNullOrEmpty(klic) ||
!vychoziMapa.TryGetValue(klic, out var vzor) ||
!StejnyObsah(polozka, vzor))
vrstva.Vlastni.Add(polozka);
}
foreach (var klic in vychoziMapa.Keys)
if (!string.IsNullOrEmpty(klic) && !klicStavu.Contains(klic))
vrstva.Skryte.Add(klic);
return vrstva;
}
/// <summary>Porovnání obsahu přes serializaci jedna metoda místo šesti implementací rovnosti
/// a automaticky pokryje i pole, která k položkám teprve přibudou.</summary>
private static bool StejnyObsah<T>(T a, T b)
=> JsonSerializer.Serialize(a, Opts) == JsonSerializer.Serialize(b, Opts);
private static void Replace<T>(ObservableCollection<T> target, IEnumerable<T> src) private static void Replace<T>(ObservableCollection<T> target, IEnumerable<T> src)
{ {
@@ -72,8 +221,11 @@ public static class CiselnikyStore
foreach (var x in src) target.Add(x); foreach (var x in src) target.Add(x);
} }
/// <summary>Přechodný tvar pro čtení settable List&lt;T&gt; (na rozdíl od get-only kolekcí v // ---- Přenosové tvary ----
/// <see cref="Ciselniky"/>). Názvy vlastností musí odpovídat <see cref="Ciselniky"/> i zápisu.</summary>
/// <summary>Tvar dodávané vrstvy (i starého jednovrstvého souboru) settable List&lt;T&gt;
/// na rozdíl od get-only kolekcí v <see cref="Ciselniky"/>, které System.Text.Json neplní.
/// Názvy vlastností musí odpovídat <see cref="Ciselniky"/>.</summary>
private sealed class Dto private sealed class Dto
{ {
public List<Organizace> Organizace { get; set; } = new(); public List<Organizace> Organizace { get; set; } = new();
@@ -82,5 +234,36 @@ public static class CiselnikyStore
public List<VerzeDokumentace> VerzeDokumentace { get; set; } = new(); public List<VerzeDokumentace> VerzeDokumentace { get; set; } = new();
public List<Cast> Casti { get; set; } = new(); public List<Cast> Casti { get; set; } = new();
public List<Kraj> Kraje { get; set; } = new(); public List<Kraj> Kraje { get; set; } = new();
public static Dto Z(Ciselniky c) => new()
{
Organizace = c.Organizace.ToList(),
Firmy = c.Firmy.ToList(),
Jmena = c.Jmena.ToList(),
VerzeDokumentace = c.VerzeDokumentace.ToList(),
Casti = c.Casti.ToList(),
Kraje = c.Kraje.ToList(),
};
}
/// <summary>Jeden číselník v uživatelské vrstvě: co uživatel přidal nebo upravil
/// (<see cref="Vlastni"/>) a které dodávané položky si odklidil (<see cref="Skryte"/> klíče).</summary>
internal sealed class Vrstva<T>
{
public List<T> Vlastni { get; set; } = new();
public List<string> Skryte { get; set; } = new();
}
/// <summary>Tvar uživatelské vrstvy. <see cref="Verze"/> je rezerva pro budoucí změnu formátu
/// bez ní by starší soubor nešlo odlišit od nového.</summary>
private sealed class UzivatelskyDto
{
public int Verze { get; set; } = 1;
public Vrstva<Organizace> Organizace { get; set; } = new();
public Vrstva<Firma> Firmy { get; set; } = new();
public Vrstva<Jmeno> Jmena { get; set; } = new();
public Vrstva<VerzeDokumentace> VerzeDokumentace { get; set; } = new();
public Vrstva<Cast> Casti { get; set; } = new();
public Vrstva<Kraj> Kraje { get; set; } = new();
} }
} }
+5 -5
View File
@@ -1,12 +1,12 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Windows; using System.Windows;
namespace Rozpisky.Data; namespace Rozpisky.Data;
/// <summary> /// <summary>
/// Společné zacházení s JSON konfiguračními soubory vedle .exe (<c>ciselniky.json</c>, /// Společné zacházení s JSON konfiguračními soubory (<c>ciselniky.user.json</c>, <c>cad.json</c>,
/// <c>cad.json</c>, <c>layout.json</c>, <c>mapovani.json</c>). Řeší dvě slabiny přímého /// <c>layout.json</c> v uživatelské složce, <c>mapovani.json</c> vedle .exe). Řeší dvě slabiny přímého
/// <c>File.WriteAllText</c>: pád uprostřed zápisu nesmí poškodit původní soubor (atomický zápis /// <c>File.WriteAllText</c>: pád uprostřed zápisu nesmí poškodit původní soubor (atomický zápis
/// přes .tmp) a poškozený soubor se nesmí tiše zahodit ručně editovaný config s překlepem by se /// přes .tmp) a poškozený soubor se nesmí tiše zahodit ručně editovaný config s překlepem by se
/// jinak bez varování přepsal výchozími hodnotami a uživatel by přišel o vlastní data. /// jinak bez varování přepsal výchozími hodnotami a uživatel by přišel o vlastní data.
@@ -41,7 +41,7 @@ public static class JsonConfigFile
(zaloha is null (zaloha is null
? "Zálohu původního souboru se nepodařilo vytvořit." ? "Zálohu původního souboru se nepodařilo vytvořit."
: $"Původní obsah je zálohovaný v:\n{zaloha}") + : $"Původní obsah je zálohovaný v:\n{zaloha}") +
"\n\nPodrobnosti jsou v souboru error.log vedle aplikace."; $"\n\nPodrobnosti jsou v souboru error.log ve složce\n{UzivatelskaData.Slozka}";
// BeginInvoke: hláška se ukáže až po rozběhnutí UI smyčky (config se čte před oknem). // BeginInvoke: hláška se ukáže až po rozběhnutí UI smyčky (config se čte před oknem).
Application.Current?.Dispatcher.BeginInvoke(() => Application.Current?.Dispatcher.BeginInvoke(() =>
MessageBox.Show(zprava, "Poškozený konfigurační soubor", MessageBox.Show(zprava, "Poškozený konfigurační soubor",
@@ -54,7 +54,7 @@ public static class JsonConfigFile
{ {
try try
{ {
File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "error.log"), File.AppendAllText(UzivatelskaData.Cesta("error.log"),
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Config: {text}{Environment.NewLine}{Environment.NewLine}", $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Config: {text}{Environment.NewLine}{Environment.NewLine}",
Encoding.UTF8); Encoding.UTF8);
} }
+60
View File
@@ -0,0 +1,60 @@
using System.IO;
namespace Rozpisky.Data;
/// <summary>
/// Jednorázový přesun dat starších verzí, které zapisovaly vedle .exe, do uživatelské složky
/// (<see cref="UzivatelskaData"/>). Bez toho by uživatel po aktualizaci našel prázdné číselníky
/// a přenastavený CAD. Volá se jednou při startu, ještě než si na data sáhne kterýkoli store.
/// <para>
/// Zásada: nikdy nic nemazat a nikdy nepřepsat novější data. Soubor se přenese jen tehdy, když
/// v cíli ještě není; originál zůstane ležet jako <c>*.migrovano</c> (jediná kopie uživatelových dat).
/// Selhání nesmí shodit start horší než nepřenesená data je aplikace, která nenaběhne.
/// </para>
/// </summary>
public static class Migrace
{
/// <summary>Přípona, kterou dostane původní soubor po přenesení.</summary>
public const string PriponaHotovo = ".migrovano";
/// <summary>Přenese <c>cad.json</c> a <c>layout.json</c> ze složky aplikace do uživatelské.
/// Číselníky mají vlastní cestu (rozpad na vrstvy) viz <see cref="CiselnikyStore"/>.</summary>
public static void PrenesStaraData()
{
Prenes(CadEnginesStoreNazev);
Prenes(LayoutConfigStoreNazev);
}
private const string CadEnginesStoreNazev = "cad.json";
private const string LayoutConfigStoreNazev = "layout.json";
/// <summary>Přesune soubor ze složky aplikace do uživatelské, pokud tam ještě není.
/// V portable režimu jsou obě složky totožné a neudělá se nic.</summary>
private static void Prenes(string nazevSouboru)
{
try
{
var zdroj = Path.Combine(AppContext.BaseDirectory, nazevSouboru);
var cil = UzivatelskaData.Cesta(nazevSouboru);
if (string.Equals(zdroj, cil, StringComparison.OrdinalIgnoreCase)) return;
if (!File.Exists(zdroj) || File.Exists(cil)) return;
File.Copy(zdroj, cil);
OznacJakoPrenesene(zdroj);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// Nezapisovatelná instalační složka apod. data zůstanou tam, kde jsou, a zkusí se
// přenést zase příště. Start aplikace to zastavit nesmí.
}
}
/// <summary>Přejmenuje původní soubor na <c>*.migrovano</c>, aby se přenos neopakoval a aby
/// starší verze programu (kdyby ji uživatel vrátil) nezmátla už neaktuální data. Nedaří-li se
/// to (soubor jen pro čtení), nevadí kopie v cíli už existuje a další běh přenos přeskočí.</summary>
internal static void OznacJakoPrenesene(string cesta)
{
try { File.Move(cesta, cesta + PriponaHotovo, overwrite: true); }
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { }
}
}
+53
View File
@@ -0,0 +1,53 @@
using System.IO;
namespace Rozpisky.Data;
/// <summary>
/// Jediné místo pro data, která píše <b>uživatel</b> (číselníky, výběr CADu, konfigurace layoutů,
/// logy). Leží v profilu uživatele (<c>%APPDATA%\Rozpisky</c>), ne vedle .exe přeinstalace novou
/// verzí ani .exe v <c>Program Files</c> (jen pro čtení) je tak nemůžou zničit.
/// <para>
/// Podklady <b>dodávané</b> s programem (šablona, mustr, loga, výchozí číselníky, mapování) zůstávají
/// vedle .exe a čtou se přes <see cref="AppContext.BaseDirectory"/> ty se naopak při každé instalaci
/// obnovují. Hranice mezi tím, co se instaluje, a tím, co patří uživateli, vede přesně tudy.
/// </para>
/// <para>
/// <b>Portable režim:</b> leží-li vedle .exe soubor <c>portable.txt</c>, vrátí se složka aplikace.
/// Slouží k běhu z USB a k vývoji, kdy je pohodlnější mít všechno v jedné složce s buildem.
/// </para>
/// </summary>
public static class UzivatelskaData
{
/// <summary>Značka portable režimu vedle .exe (obsah souboru se neřeší, stačí že existuje).</summary>
public const string PortableZnacka = "portable.txt";
private const string NazevSlozky = "Rozpisky";
private static string? _slozka;
/// <summary>Složka pro zapisovaná data. Počítá se jednou za běh a při prvním použití se vytvoří.
/// Když se vytvořit nepodaří (výjimečné profil uživatele je zapisovatelný), spadne se zpátky
/// na složku aplikace, ať se program aspoň spustí.</summary>
public static string Slozka => _slozka ??= UrciSlozku();
/// <summary>Plná cesta k souboru se zapisovanými daty.</summary>
public static string Cesta(string nazevSouboru) => Path.Combine(Slozka, nazevSouboru);
private static string UrciSlozku()
{
var vedleExe = AppContext.BaseDirectory;
if (File.Exists(Path.Combine(vedleExe, PortableZnacka))) return vedleExe;
try
{
var slozka = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), NazevSlozky);
Directory.CreateDirectory(slozka);
return slozka;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return vedleExe;
}
}
}
+20 -12
View File
@@ -23,20 +23,28 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- DXF šablona rozpisky (leží v kořeni solution) kopíruje se vedle .exe. --> <!-- Slučování vrstev číselníků (CiselnikyStore.Sluc/Rozdil) je čistá logika, na které závisí,
<None Include="..\Rozpiska.dxf" Link="Rozpiska.dxf" CopyToOutputDirectory="PreserveNewest" /> jestli uživatel po aktualizaci najde svoje záznamy testuje se přímo, bez souborů. -->
<InternalsVisibleTo Include="Rozpisky.Tests" />
</ItemGroup>
<!-- Podklady dodávané s programem (Podklady\) kopírují se vedle .exe do ploché struktury
(+ podsložka loga\). Link určuje umístění ve výstupu; zdrojový strom je tím oddělený
od rozvržení instalace. Aplikace je jen čte, nikdy do nich nezapisuje. -->
<ItemGroup>
<!-- DXF šablona rozpisky. -->
<None Update="Podklady\Rozpiska.dxf" Link="Rozpiska.dxf" CopyToOutputDirectory="PreserveNewest" />
<!-- Mustr „Seznam příloh“ (Otevřená verze .xlsx) šablona vyplňovaná při exportu seznamu. --> <!-- Mustr „Seznam příloh“ (Otevřená verze .xlsx) šablona vyplňovaná při exportu seznamu. -->
<None Include="..\SEZNAM.xlsx" Link="SEZNAM.xlsx" CopyToOutputDirectory="PreserveNewest" /> <None Update="Podklady\SEZNAM.xlsx" Link="SEZNAM.xlsx" CopyToOutputDirectory="PreserveNewest" />
<!-- Závazný manuál SŽ (PDF) kopíruje se vedle .exe pro otevření z nabídky Nápověda. --> <!-- Závazný manuál SŽ (PDF) otevírá se z nabídky Nápověda. -->
<None Include="..\SZ_SM011_P10_Manual_v6.pdf" Link="SZ_SM011_P10_Manual_v6.pdf" CopyToOutputDirectory="PreserveNewest" /> <None Update="Podklady\SZ_SM011_P10_Manual_v6.pdf" Link="SZ_SM011_P10_Manual_v6.pdf" CopyToOutputDirectory="PreserveNewest" />
<!-- Výchozí číselníky (data z původního Excelu) dodávají se vedle .exe. <!-- Dodávaná vrstva číselníků (data z původního Excelu). Aplikace ji jen čte uživatelovy
Update (ne Include): .json je už implicitní None item. PreserveNewest = uživatelovy záznamy žijí odděleně v ciselniky.user.json v jeho profilu, viz CiselnikyStore. -->
úpravy v output složce se při rebuildu nepřepíšou (přepíše jen novější zdroj). --> <None Update="Podklady\ciselniky.vychozi.json" Link="ciselniky.vychozi.json" CopyToOutputDirectory="PreserveNewest" />
<None Update="ciselniky.json" CopyToOutputDirectory="PreserveNewest" />
<!-- Mapování „pole programu → DXF atribut“ (RozpiskaValues přes reflexi). Editovatelné vedle .exe. --> <!-- Mapování „pole programu → DXF atribut“ (RozpiskaValues přes reflexi). Editovatelné vedle .exe. -->
<None Update="mapovani.json" CopyToOutputDirectory="PreserveNewest" /> <None Update="Podklady\mapovani.json" Link="mapovani.json" CopyToOutputDirectory="PreserveNewest" />
<!-- Loga dodávaná s programem (DXF) kopírují se do složky loga\ vedle .exe. --> <!-- Loga dodávaná s programem (DXF) do podsložky loga\ vedle .exe. -->
<None Include="loga\**\*" CopyToOutputDirectory="PreserveNewest" LinkBase="loga" /> <None Update="Podklady\loga\*" Link="loga\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+8 -17
View File
@@ -1,4 +1,4 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
@@ -511,33 +511,24 @@ public sealed class MainViewModel : ObservableObject
// ---- Sdílené číselníky (mimo projekt, vedle .exe) ---- // ---- Sdílené číselníky (mimo projekt, vedle .exe) ----
/// <summary>Načte číselníky z <c>ciselniky.json</c> vedle .exe; když chybí, nasází výchozí obsah /// <summary>Načte číselníky: dodávaná vrstva vedle .exe + rozdíl uživatele z jeho složky
/// a soubor vytvoří. Pak zapne autosave při přidání/odebrání položky.</summary> /// (<see cref="CiselnikyStore"/>). Pak zapne autosave při přidání/odebrání položky.</summary>
private void NactiCiselniky() private void NactiCiselniky()
{ {
if (!CiselnikyStore.LoadInto(Ciselniky)) CiselnikyStore.Nacti(Ciselniky);
{
DemoData.SeedCiselniky(Ciselniky);
UlozCiselniky(); // vytvoř výchozí soubor
}
else if (Ciselniky.Kraje.Count == 0)
{
DemoData.SeedKraje(Ciselniky); // kraje přibyly později → doplň i do starších ciselniky.json
UlozCiselniky();
}
// Autosave na přidání/odebrání položky; úpravy v buňkách se uloží při zavření okna. // Autosave na přidání/odebrání položky; úpravy v buňkách se uloží při zavření okna.
foreach (var col in new INotifyCollectionChanged[] foreach (var col in new INotifyCollectionChanged[]
{ Ciselniky.Organizace, Ciselniky.Firmy, Ciselniky.Jmena, Ciselniky.VerzeDokumentace, Ciselniky.Casti, Ciselniky.Kraje }) { Ciselniky.Organizace, Ciselniky.Firmy, Ciselniky.Jmena, Ciselniky.VerzeDokumentace, Ciselniky.Casti, Ciselniky.Kraje })
col.CollectionChanged += (_, _) => UlozCiselniky(); col.CollectionChanged += (_, _) => UlozCiselniky();
} }
/// <summary>Uloží číselníky vedle .exe. Při autosave záměrně bez dialogu (read-only složka u .exe /// <summary>Uloží rozdíl číselníků proti dodávané vrstvě do složky uživatele. Při autosave
/// v Program Files nesmí spamovat MessageBoxy) selhání se ale ukáže ve stavovém řádku, /// záměrně bez dialogu (nesmí spamovat MessageBoxy) selhání se ale ukáže ve stavovém řádku,
/// ať uživatel ví, že se změny číselníků neukládají. Volá se i při zavření okna.</summary> /// ať uživatel ví, že se změny číselníků neukládají. Volá se i při zavření okna.</summary>
public void UlozCiselniky() public void UlozCiselniky()
{ {
try { CiselnikyStore.Save(Ciselniky); } try { CiselnikyStore.UlozRozdil(Ciselniky); }
catch { StavovaZprava = "Číselníky se nepodařilo uložit (složka aplikace je jen pro čtení?)."; } catch { StavovaZprava = $"Číselníky se nepodařilo uložit do složky {UzivatelskaData.Slozka}."; }
} }
/// <summary>„Samoučení" našeptávače: jména použitá v projektu/přílohách, která v číselníku /// <summary>„Samoučení" našeptávače: jména použitá v projektu/přílohách, která v číselníku
+20 -10
View File
@@ -1,4 +1,4 @@
# Rozpisky rozhraní aplikace, datový model a budoucí funkcionality # Rozpisky rozhraní aplikace, datový model a budoucí funkcionality
Tento dokument popisuje **standalone aplikaci Rozpisky** z pohledu uživatele a dat: jaké má Tento dokument popisuje **standalone aplikaci Rozpisky** z pohledu uživatele a dat: jaké má
obrazovky, co se v nich vyplňuje, jaké akce nabízí a jak je vnitřně uspořádaný datový model. obrazovky, co se v nich vyplňuje, jaké akce nabízí a jak je vnitřně uspořádaný datový model.
@@ -131,12 +131,22 @@ Na revize se odkazují řádkové revizní sloupce přílohy (R1R4) a revizn
| **Verze dokumentace** | verze | stupeň dokumentace | | **Verze dokumentace** | verze | stupeň dokumentace |
| **Části** | označení, název části | části/objekty v seznamu | | **Části** | označení, název části | části/objekty v seznamu |
> **Číselníky jsou sdílená data napříč projekty a žijí v samostatné databázi vedle `.exe`.** > **Číselníky jsou sdílená data napříč projekty a žijí mimo projekt.** V původním Excelu šlo
> V původním Excelu šlo o samostatné listy s daty (označení dokumentace, data firem apod.). > o samostatné listy s daty (označení dokumentace, data firem apod.). Ve standalone aplikaci jsou
> Ve standalone aplikaci se z nich stane **externí databázový soubor uložený vedle spustitelného > uložené ve **dvou vrstvách JSON** (na rozdíl od revizí, které jsou interní k zakázce
> souboru** (např. `cisleniky.db` / `.json` ve složce aplikace), aby ho šlo **průběžně doplňovat > viz [3.3](#33-revize-původní-list-revize)):
> a sdílet** mezi projekty bez zásahu do aplikace. (Na rozdíl od revizí, které jsou interní >
> k zakázce viz [3.3](#33-revize-původní-list-revize).) > | Vrstva | Soubor | Role |
> |---|---|---|
> | dodávaná | `ciselniky.vychozi.json` vedle `.exe` | výchozí obsah dodaný s programem, jen ke čtení; každá instalace ho obnoví |
> | uživatelská | `ciselniky.user.json` v `%APPDATA%\Rozpisky` | jen **rozdíl** proti dodávané: `Vlastni` (přidané a upravené položky) + `Skryte` (klíče odklizených dodávaných položek) |
>
> Při načtení se vrstvy slučují: dodávané pořadí zůstává, upravená položka se přepíše na svém
> místě, vlastní se řadí za ně. Při uložení se rozdíl dopočítá znovu. Smysl rozdělení: uživatel
> číselníky průběžně doplňuje (ručně i samoučením jmen z projektů) a **přeinstalace novou verzí
> mu je nesmí přepsat** zároveň smí nová verze dodávaný číselník libovolně měnit.
> Klíč položky: `Nazev` (organizace, firmy, kraje), `Hodnota` (jména, verze), `Oznaceni` (části);
> porovnává se bez ohledu na velikost písmen.
### 3.5 Konfigurace ### 3.5 Konfigurace
@@ -153,7 +163,7 @@ Na revize se odkazují řádkové revizní sloupce přílohy (R1R4) a revizn
### 3.6 Mapování `pole aplikace ↔ DXF ATTDEF tag` ### 3.6 Mapování `pole aplikace ↔ DXF ATTDEF tag`
Šablona [`Rozpiska.dxf`](Rozpiska.dxf) obsahuje **101 polí (`ATTDEF`)**. Tag pole = klíč do Šablona [`Rozpiska.dxf`](../Rozpisky/Podklady/Rozpiska.dxf) obsahuje **101 polí (`ATTDEF`)**. Tag pole = klíč do
`values` při vyplnění (viz `DxfTemplate.Replay(g, values)` v `ROZPISKY.md`). Mapování na zdroj dat: `values` při vyplnění (viz `DxfTemplate.Replay(g, values)` v `ROZPISKY.md`). Mapování na zdroj dat:
**Projektová pole (z údajů projektu):** **Projektová pole (z údajů projektu):**
@@ -396,8 +406,8 @@ Orientační fázování (priorita shora dolů):
- **Formát uložení** rozdělené úložiště: - **Formát uložení** rozdělené úložiště:
- **Projekt** (údaje + seznam příloh + **revize**) = interní data jedné zakázky → formát k rozhodnutí - **Projekt** (údaje + seznam příloh + **revize**) = interní data jedné zakázky → formát k rozhodnutí
(JSON / SQLite / vlastní). (JSON / SQLite / vlastní).
- **Číselníky** (organizace, firmy, jména, profese, verze, části) = **externí databáze vedle `.exe`**, - ~~**Číselníky** formát a umístění.~~ **Vyřešeno:** dvě vrstvy JSON dodávaná vedle `.exe`,
sdílená a doplňovatelná napříč projekty ([3.4](#34-číselníky)) → formát k rozhodnutí. uživatelská (rozdíl) v `%APPDATA%\Rozpisky` ([3.4](#34-číselníky)).
- **Generátor 47-pozičního kódu** doimplementovat dle manuálu kap. 3.2.13.2.8 vč. okrajových případů - **Generátor 47-pozičního kódu** doimplementovat dle manuálu kap. 3.2.13.2.8 vč. okrajových případů
(sdružené objekty `SK`/`PK`, kap. 3.3; pracovní vs. definitivní verze). (sdružené objekty `SK`/`PK`, kap. 3.3; pracovní vs. definitivní verze).
- **Orientační schéma** jak se obrázek schématu získá/přiřadí k příloze (vstup uživatele vs. generování). - **Orientační schéma** jak se obrázek schématu získá/přiřadí k příloze (vstup uživatele vs. generování).
+33 -17
View File
@@ -1,4 +1,4 @@
# Rozpisky generátor výkresových rohových razítek (C# / .NET) # Rozpisky generátor výkresových rohových razítek (C# / .NET)
Standalone WPF aplikace (náhrada původního excelového nástroje „Rozpisky 1.0.8"). Spravuje projekt, Standalone WPF aplikace (náhrada původního excelového nástroje „Rozpisky 1.0.8"). Spravuje projekt,
seznam příloh, revize a číselníky, a z **DXF šablony rozpisky** generuje **náhled**, **PDF na A4**, seznam příloh, revize a číselníky, a z **DXF šablony rozpisky** generuje **náhled**, **PDF na A4**,
@@ -6,9 +6,9 @@ seznam příloh, revize a číselníky, a z **DXF šablony rozpisky** generuje *
i **tisk rozvržení**. i **tisk rozvržení**.
Celé kreslení stojí na jedné myšlence: existuje jedno **kreslicí rozhraní** Celé kreslení stojí na jedné myšlence: existuje jedno **kreslicí rozhraní**
([`IProfileRenderer`](Rozpisky/Rendering/IProfileRenderer.cs) „kam se kreslí") a několik jeho ([`IProfileRenderer`](../Rozpisky/Rendering/IProfileRenderer.cs) „kam se kreslí") a několik jeho
implementací. Tatáž kresba teče na obrazovku (náhled), do PNG, do PDF i do DXF. Čtečka šablony implementací. Tatáž kresba teče na obrazovku (náhled), do PNG, do PDF i do DXF. Čtečka šablony
([`DxfTemplate`](Rozpisky/Dxf/DxfTemplate.cs)) jen čte DXF a „přehrává" ho do tohoto rozhraní. ([`DxfTemplate`](../Rozpisky/Dxf/DxfTemplate.cs)) jen čte DXF a „přehrává" ho do tohoto rozhraní.
--- ---
@@ -39,7 +39,7 @@ zvláštní `PlatformTarget` se nenastavuje.
- Pole, která byla v šabloně `ATTDEF`, se při vyplnění zapisují jako obyčejné `TEXT`/`MTEXT` entity - Pole, která byla v šabloně `ATTDEF`, se při vyplnění zapisují jako obyčejné `TEXT`/`MTEXT` entity
(žádné atributy bloku). (žádné atributy bloku).
- Objekty v netisknutelných hladinách (`PlotFlag=0`) se **nevykreslují**, ale ví se o nich kvůli - Objekty v netisknutelných hladinách (`PlotFlag=0`) se **nevykreslují**, ale ví se o nich kvůli
místům pro loga (viz [`DxfTemplate.PlaceholderHladiny`](Rozpisky/Dxf/DxfTemplate.cs)). místům pro loga (viz [`DxfTemplate.PlaceholderHladiny`](../Rozpisky/Dxf/DxfTemplate.cs)).
--- ---
@@ -73,8 +73,17 @@ Rozpisky/
└─ MainWindow.xaml(.cs) ← hlavní okno └─ MainWindow.xaml(.cs) ← hlavní okno
``` ```
Vedle .exe se dodávají editovatelné soubory: `Rozpiska.dxf` (šablona), `SEZNAM.xlsx` (mustr seznamu), Datové soubory jsou rozdělené podle toho, kdo je vlastní:
`ciselniky.json`, `mapovani.json`, `cad.json`, `layout.json`, `loga/` a manuál SŽ (PDF).
| Kde | Co | Kdo zapisuje |
|---|---|---|
| vedle `.exe` (zdroj `Rozpisky/Podklady/`) | `Rozpiska.dxf`, `SEZNAM.xlsx`, `ciselniky.vychozi.json`, `mapovani.json`, `loga/`, manuál SŽ (PDF) | nikdo dodává se s programem, každá instalace přepíše |
| `%APPDATA%\Rozpisky` ([`UzivatelskaData`](../Rozpisky/Data/UzivatelskaData.cs)) | `ciselniky.user.json`, `cad.json`, `layout.json`, `error.log`, `tisk_diag.log` | aplikace za běhu |
| kdekoli u uživatele | projekty `.rzp` | aplikace na vyžádání |
Rozdělení existuje proto, aby přeinstalace novou verzí (nebo `.exe` v `Program Files`, kam se nesmí
psát) nesmazala nic, co si uživatel nastřádal. Portable režim se zapíná souborem `portable.txt`
vedle `.exe` pak se všechno vrátí do složky aplikace.
--- ---
@@ -122,7 +131,7 @@ Implementace:
## 4. Čtečka šablony a fasáda pro UI ## 4. Čtečka šablony a fasáda pro UI
Nízkoúrovňová čtečka [`DxfTemplate`](Rozpisky/Dxf/DxfTemplate.cs): Nízkoúrovňová čtečka [`DxfTemplate`](../Rozpisky/Dxf/DxfTemplate.cs):
```csharp ```csharp
var tpl = new DxfTemplate("Rozpiska.dxf"); var tpl = new DxfTemplate("Rozpiska.dxf");
@@ -136,7 +145,7 @@ tpl.Replay(g, values, includeNoPrint); // + volitelně A4 rámeček !!NOPRINT (p
vystředění), `Placeholders` vrací rámečky pro loga/schéma podle vyhrazených hladin, `Bounds` bbox vystředění), `Placeholders` vrací rámečky pro loga/schéma podle vyhrazených hladin, `Bounds` bbox
kresby (pro fit vkládaného fragmentu). kresby (pro fit vkládaného fragmentu).
Fasáda pro UI [`RozpiskaRenderer`](Rozpisky/Dxf/RozpiskaRenderer.cs) spojuje šablonu, mapování hodnot Fasáda pro UI [`RozpiskaRenderer`](../Rozpisky/Dxf/RozpiskaRenderer.cs) spojuje šablonu, mapování hodnot
a exportéry na jedno místo (šablona se cachuje): a exportéry na jedno místo (šablona se cachuje):
```csharp ```csharp
@@ -147,16 +156,16 @@ r.ExportPdf(projekt, priloha, revize, ciselniky, "rozpiska.pdf", loga); // A4
r.ExportPng(projekt, priloha, revize, ciselniky, "nahled.png", loga); // A4 PNG r.ExportPng(projekt, priloha, revize, ciselniky, "nahled.png", loga); // A4 PNG
``` ```
Hodnoty polí sestavuje [`RozpiskaValues`](Rozpisky/Dxf/RozpiskaValues.cs): mapování „pole programu → Hodnoty polí sestavuje [`RozpiskaValues`](../Rozpisky/Dxf/RozpiskaValues.cs): mapování „pole programu →
DXF atribut" **není v kódu**, čte se z `mapovani.json` a hodnoty se dotahují reflexí. K tomu se DXF atribut" **není v kódu**, čte se z `mapovani.json` a hodnoty se dotahují reflexí. K tomu se
algoritmicky doplní 47-poziční strojový kód (`K1``K47`, viz algoritmicky doplní 47-poziční strojový kód (`K1``K47`, viz
[`KodPrilohy`](Rozpisky/Dxf/KodPrilohy.cs)). [`KodPrilohy`](../Rozpisky/Dxf/KodPrilohy.cs)).
--- ---
## 5. Export na A4 (`Exporters`) ## 5. Export na A4 (`Exporters`)
[`Exporters`](Rozpisky/Dxf/Exporters.cs) vykreslí **libovolný** `Action<IProfileRenderer>`: [`Exporters`](../Rozpisky/Dxf/Exporters.cs) vykreslí **libovolný** `Action<IProfileRenderer>`:
```csharp ```csharp
Exporters.PngA4(draw, "rozpiska.png", dpi: 200, frame: sheetFrame); // A4 raster Exporters.PngA4(draw, "rozpiska.png", dpi: 200, frame: sheetFrame); // A4 raster
@@ -172,7 +181,7 @@ přes `SKDocument.CreatePdf` (1 mm = 72/25,4 bodu; text vektorový, tloušťky v
## 6. Demo data (`DemoData`) ## 6. Demo data (`DemoData`)
[`DemoData`](Rozpisky/Data/DemoData.cs) naplní úvodní stav při startu: [`DemoData`](../Rozpisky/Data/DemoData.cs) naplní úvodní stav při startu:
`SeedProjekt(vm)` (ukázkový projekt + přílohy), `SeedCiselniky(c)` a `SeedKraje(c)` (výchozí `SeedProjekt(vm)` (ukázkový projekt + přílohy), `SeedCiselniky(c)` a `SeedKraje(c)` (výchozí
číselníky, pokud `ciselniky.json` chybí). číselníky, pokud `ciselniky.json` chybí).
@@ -190,18 +199,25 @@ přes `SKDocument.CreatePdf` (1 mm = 72/25,4 bodu; text vektorový, tloušťky v
| Aktualizace/tisk rozvržení | přílohy | přejmenování/tisk v CADu | `Cad/CadLayoutService`, `Cad/CadTiskService` (COM) | | Aktualizace/tisk rozvržení | přílohy | přejmenování/tisk v CADu | `Cad/CadLayoutService`, `Cad/CadTiskService` (COM) |
Aplikace **nemá CLI režim** vše jede z GUI. Neočekávané výjimky zachytává globální síť v Aplikace **nemá CLI režim** vše jede z GUI. Neočekávané výjimky zachytává globální síť v
[`App.xaml.cs`](Rozpisky/App.xaml.cs) a zapisuje je do `error.log` vedle .exe. [`App.xaml.cs`](../Rozpisky/App.xaml.cs) a zapisuje je do `error.log` vedle .exe.
--- ---
## 8. Persistence a robustnost ## 8. Persistence a robustnost
- **Projekt `.rzp`** ([`ProjektStore`](Rozpisky/Data/ProjektStore.cs)) JSON s exkluzivním zámkem - **Projekt `.rzp`** ([`ProjektStore`](../Rozpisky/Data/ProjektStore.cs)) JSON s exkluzivním zámkem
drženým po dobu otevření (ostatní procesy smí jen číst). Zápis serializuje **před** useknutím drženým po dobu otevření (ostatní procesy smí jen číst). Zápis serializuje **před** useknutím
souboru, aby pád nezanechal prázdný soubor. souboru, aby pád nezanechal prázdný soubor.
- **Configy vedle .exe** (`ciselniky.json`, `cad.json`, `layout.json`, `mapovani.json`) jdou přes - **JSON configy** (`ciselniky.user.json`, `cad.json`, `layout.json` v uživatelské složce,
[`JsonConfigFile`](Rozpisky/Data/JsonConfigFile.cs): **atomický zápis** (přes `.tmp`) a při `mapovani.json` vedle .exe) jdou přes [`JsonConfigFile`](../Rozpisky/Data/JsonConfigFile.cs):
poškozeném souboru **záloha do `.bak` + upozornění** (místo tichého přepsání výchozími daty). **atomický zápis** (přes `.tmp`) a při poškozeném souboru **záloha do `.bak` + upozornění**
(místo tichého přepsání výchozími daty).
- **Číselníky ve dvou vrstvách** ([`CiselnikyStore`](../Rozpisky/Data/CiselnikyStore.cs)) dodávaná
`ciselniky.vychozi.json` (jen ke čtení) a uživatelská `ciselniky.user.json`, kde je uložený jen
**rozdíl** proti ní (přidané/upravené položky + klíče skrytých). Nová verze programu tak smí
dodávaný číselník měnit, aniž by uživatel přišel o svoje záznamy.
- **Migrace starých poloh** ([`Migrace`](../Rozpisky/Data/Migrace.cs)) data zapisovaná dřív vedle
.exe se při startu přenesou do uživatelské složky; originál se jen přejmenuje na `.migrovano`.
- **Zamčený cíl exportu** před zápisem PDF/XLSX/DXF se ověří, zda soubor nedrží jiný program - **Zamčený cíl exportu** před zápisem PDF/XLSX/DXF se ověří, zda soubor nedrží jiný program
(Excel, prohlížeč PDF), a nabídne Pokračovat/Přerušit. (Excel, prohlížeč PDF), a nabídne Pokračovat/Přerušit.
View File