diff --git a/Rozpisky.Tests/CiselnikyVrstvyTesty.cs b/Rozpisky.Tests/CiselnikyVrstvyTesty.cs
new file mode 100644
index 0000000..b3e9d1f
--- /dev/null
+++ b/Rozpisky.Tests/CiselnikyVrstvyTesty.cs
@@ -0,0 +1,191 @@
+using System.Collections.ObjectModel;
+using Rozpisky.Data;
+using Rozpisky.Models;
+using Xunit;
+
+namespace Rozpisky.Tests;
+
+///
+/// Slučování dodávané a uživatelské vrstvy číselníků a dopočet rozdílu
+/// ( / ).
+/// 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í.
+///
+public class CiselnikyVrstvyTesty
+{
+ private static List Vychozi(params string[] jmena)
+ => jmena.Select(j => new Jmeno { Hodnota = j }).ToList();
+
+ private static ObservableCollection Sluc(
+ List vychozi, CiselnikyStore.Vrstva? uziv)
+ {
+ var cil = new ObservableCollection();
+ 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 { 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 { 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
+ {
+ new() { Nazev = "SŽ Praha", Adresa = "stará" },
+ new() { Nazev = "SŽ Brno", Adresa = "Kounicova 26" },
+ };
+ var uziv = new CiselnikyStore.Vrstva
+ {
+ Vlastni = { new Organizace { Nazev = "SŽ Praha", Adresa = "Dlážděná 1003/7" } },
+ };
+
+ var cil = new ObservableCollection();
+ 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 { 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 { 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
+ {
+ new() { Nazev = "EXprojekt s.r.o.", Telefon = "+420 533 312 000" },
+ };
+ var stav = new List
+ {
+ 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ý" }));
+ }
+}
diff --git a/Rozpisky/Data/CiselnikyStore.cs b/Rozpisky/Data/CiselnikyStore.cs
index 4224fbb..e74ed3b 100644
--- a/Rozpisky/Data/CiselnikyStore.cs
+++ b/Rozpisky/Data/CiselnikyStore.cs
@@ -1,4 +1,4 @@
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
@@ -7,64 +7,213 @@ using Rozpisky.Models;
namespace Rozpisky.Data;
///
-/// Načítání/ukládání sdílených číselníků (organizace, firmy, jména, verze, části)
-/// do JSON souboru vedle .exe (portable). Číselníky jsou společné všem projektům
-/// (na rozdíl od .rzp přes ), takže žijí mimo projekt.
-/// Bez NuGet závislostí – System.Text.Json je součást .NET.
+/// Sdílené číselníky (organizace, firmy, jména, verze, části, kraje) ve dvou vrstvách:
+///
+/// - dodávaná – ciselniky.vychozi.json vedle .exe, jen ke čtení; každá instalace
+/// ji obnoví, takže smí s novou verzí programu růst a měnit se;
+/// - uživatelská – ciselniky.user.json v uživatelské složce
+/// (), kde je uložený jen rozdíl proti dodávané vrstvě.
+///
+/// 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é.
+/// Bez NuGet závislostí – System.Text.Json je součást .NET.
///
public static class CiselnikyStore
{
- public const string SouborNazev = "ciselniky.json";
+ /// Dodávaná vrstva vedle .exe (jen ke čtení).
+ public const string VychoziNazev = "ciselniky.vychozi.json";
+
+ /// Uživatelská vrstva (rozdíl proti dodávané) v uživatelské složce.
+ public const string UzivatelskyNazev = "ciselniky.user.json";
+
+ /// Jednovrstvý soubor starších verzí vedle .exe – zdroj pro jednorázovou migraci.
+ public const string StaryNazev = "ciselniky.json";
private static readonly JsonSerializerOptions Opts = new()
{
WriteIndented = true,
- // Diakritika čitelně (ě š č…), ne jako \uXXXX.
+ // Diakritika čitelně (ě š č…), ne jako escape sekvence.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
- /// Výchozí umístění: soubor vedle .exe (portable). Lze přepsat parametrem cesty.
- public static string DefaultPath => Path.Combine(AppContext.BaseDirectory, SouborNazev);
+ public static string VychoziPath => Path.Combine(AppContext.BaseDirectory, VychoziNazev);
+ public static string UzivatelskyPath => UzivatelskaData.Cesta(UzivatelskyNazev);
+ public static string StaryPath => Path.Combine(AppContext.BaseDirectory, StaryNazev);
- /// Naplní cílové číselníky obsahem z JSON. Vrací false, když soubor neexistuje
- /// nebo je poškozený – volající pak nasází výchozí obsah (a uloží nový soubor).
- public static bool LoadInto(Ciselniky target, string? path = null)
+ /// Dodávaná vrstva načtená při startu. Drží se po celý běh, protože je referenčním
+ /// bodem pro dopočet rozdílu při každém uložení – kdyby se načítala znovu, změna souboru
+ /// za běhu by rozdíl rozhodila.
+ private static Dto? _vychozi;
+
+ /// Načte číselníky: dodávaná vrstva + rozdíl uživatele. Když dodávaná vrstva chybí
+ /// (poškozená instalace), použije se vestavěný , ať má
+ /// uživatel s čím pracovat. Před prvním načtením proběhne migrace ze starého jednovrstvého
+ /// souboru.
+ public static void Nacti(Ciselniky cil)
{
- path ??= DefaultPath;
- if (!File.Exists(path)) return false;
+ _vychozi = NactiVychozi();
+ 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);
+ }
+
+ /// Uloží rozdíl 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í).
+ 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(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
{
- // Čteme do DTO se settable List: System.Text.Json neplní get-only ObservableCollection.
- var data = JsonSerializer.Deserialize(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;
+ return JsonSerializer.Deserialize(File.ReadAllText(UzivatelskyPath), Opts);
}
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
- // obsahem, proto se originál nejdřív zazálohuje a uživatel dozví (jinak by o své
- // číselníky přišel tiše).
- JsonConfigFile.ZalohujAOhlasPoskozeny(path, ex);
- return false;
+ // Ruční editace s překlepem nesmí uživatelovy záznamy tiše zahodit – zazálohovat a ohlásit.
+ JsonConfigFile.ZalohujAOhlasPoskozeny(UzivatelskyPath, ex);
+ return null;
}
}
- /// Uloží číselníky do JSON (atomicky přes .tmp – pád uprostřed zápisu nesmí soubor
- /// poškodit). Chyby zápisu (např. read-only složka u .exe v Program Files) propustí volajícímu,
- /// ať je může ošetřit dle kontextu (autosave je spolkne, ruční uložení zahlásí).
- public static void Save(Ciselniky c, string? path = null)
- => JsonConfigFile.ZapisAtomicky(path ?? DefaultPath, JsonSerializer.Serialize(c, Opts));
+ /// Jednorázový převod ze starého jednovrstvého ciselniky.json vedle .exe:
+ /// jeho obsah je „aktuální stav“ uživatele, takže se z něj dopočítá rozdíl proti dodávané
+ /// vrstvě a uloží jako uživatelská vrstva. Originál se jen přejmenuje, nikdy nemaže.
+ private static void MigrujStarySoubor()
+ {
+ if (File.Exists(UzivatelskyPath) || !File.Exists(StaryPath)) return;
+ try
+ {
+ var stary = JsonSerializer.Deserialize(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 ----
+
+ /// 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.
+ 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,
+ _ => "",
+ };
+
+ /// 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ě.
+ internal static void Sluc(ObservableCollection cil, List vychozi, Vrstva? uziv)
+ {
+ var skryte = new HashSet(uziv?.Skryte ?? new List(), StringComparer.OrdinalIgnoreCase);
+ var vysledek = vychozi.Where(x => !skryte.Contains(Klic(x!))).ToList();
+
+ foreach (var vlastni in uziv?.Vlastni ?? new List())
+ {
+ 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);
+ }
+
+ /// Rozdíl aktuálního stavu proti dodávané vrstvě: co v ní není nebo se od ní liší,
+ /// jde do Vlastni; dodávaná položka, která ve stavu chybí, jde do Skryte.
+ internal static Vrstva Rozdil(IEnumerable aktualni, List 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(stav.Select(x => Klic(x!)), StringComparer.OrdinalIgnoreCase);
+
+ var vrstva = new Vrstva();
+ 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;
+ }
+
+ /// 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.
+ private static bool StejnyObsah(T a, T b)
+ => JsonSerializer.Serialize(a, Opts) == JsonSerializer.Serialize(b, Opts);
private static void Replace(ObservableCollection target, IEnumerable src)
{
@@ -72,8 +221,11 @@ public static class CiselnikyStore
foreach (var x in src) target.Add(x);
}
- /// Přechodný tvar pro čtení – settable List<T> (na rozdíl od get-only kolekcí v
- /// ). Názvy vlastností musí odpovídat i zápisu.
+ // ---- Přenosové tvary ----
+
+ /// Tvar dodávané vrstvy (i starého jednovrstvého souboru) – settable List<T>
+ /// na rozdíl od get-only kolekcí v , které System.Text.Json neplní.
+ /// Názvy vlastností musí odpovídat .
private sealed class Dto
{
public List Organizace { get; set; } = new();
@@ -82,5 +234,36 @@ public static class CiselnikyStore
public List VerzeDokumentace { get; set; } = new();
public List Casti { get; set; } = new();
public List 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(),
+ };
+ }
+
+ /// Jeden číselník v uživatelské vrstvě: co uživatel přidal nebo upravil
+ /// () a které dodávané položky si odklidil ( – klíče).
+ internal sealed class Vrstva
+ {
+ public List Vlastni { get; set; } = new();
+ public List Skryte { get; set; } = new();
+ }
+
+ /// Tvar uživatelské vrstvy. je rezerva pro budoucí změnu formátu –
+ /// bez ní by starší soubor nešlo odlišit od nového.
+ private sealed class UzivatelskyDto
+ {
+ public int Verze { get; set; } = 1;
+ public Vrstva Organizace { get; set; } = new();
+ public Vrstva Firmy { get; set; } = new();
+ public Vrstva Jmena { get; set; } = new();
+ public Vrstva VerzeDokumentace { get; set; } = new();
+ public Vrstva Casti { get; set; } = new();
+ public Vrstva Kraje { get; set; } = new();
}
}
diff --git a/Rozpisky/Podklady/ciselniky.json b/Rozpisky/Podklady/ciselniky.vychozi.json
similarity index 100%
rename from Rozpisky/Podklady/ciselniky.json
rename to Rozpisky/Podklady/ciselniky.vychozi.json
diff --git a/Rozpisky/Rozpisky.csproj b/Rozpisky/Rozpisky.csproj
index db9d136..e4ac35a 100644
--- a/Rozpisky/Rozpisky.csproj
+++ b/Rozpisky/Rozpisky.csproj
@@ -22,6 +22,12 @@
+
+
+
+
+
@@ -32,8 +38,9 @@
-
-
+
+
diff --git a/Rozpisky/ViewModels/MainViewModel.cs b/Rozpisky/ViewModels/MainViewModel.cs
index 8000c27..faa7abd 100644
--- a/Rozpisky/ViewModels/MainViewModel.cs
+++ b/Rozpisky/ViewModels/MainViewModel.cs
@@ -1,4 +1,4 @@
-using System.Collections.ObjectModel;
+using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
@@ -511,33 +511,24 @@ public sealed class MainViewModel : ObservableObject
// ---- Sdílené číselníky (mimo projekt, vedle .exe) ----
- /// Načte číselníky z ciselniky.json vedle .exe; když chybí, nasází výchozí obsah
- /// a soubor vytvoří. Pak zapne autosave při přidání/odebrání položky.
+ /// Načte číselníky: dodávaná vrstva vedle .exe + rozdíl uživatele z jeho složky
+ /// (). Pak zapne autosave při přidání/odebrání položky.
private void NactiCiselniky()
{
- if (!CiselnikyStore.LoadInto(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();
- }
+ CiselnikyStore.Nacti(Ciselniky);
// 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[]
{ Ciselniky.Organizace, Ciselniky.Firmy, Ciselniky.Jmena, Ciselniky.VerzeDokumentace, Ciselniky.Casti, Ciselniky.Kraje })
col.CollectionChanged += (_, _) => UlozCiselniky();
}
- /// Uloží číselníky vedle .exe. Při autosave záměrně bez dialogu (read-only složka u .exe
- /// v Program Files nesmí spamovat MessageBoxy) – selhání se ale ukáže ve stavovém řádku,
+ /// Uloží rozdíl číselníků proti dodávané vrstvě do složky uživatele. Při autosave
+ /// 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.
public void UlozCiselniky()
{
- try { CiselnikyStore.Save(Ciselniky); }
- catch { StavovaZprava = "Číselníky se nepodařilo uložit (složka aplikace je jen pro čtení?)."; }
+ try { CiselnikyStore.UlozRozdil(Ciselniky); }
+ catch { StavovaZprava = $"Číselníky se nepodařilo uložit do složky {UzivatelskaData.Slozka}."; }
}
/// „Samoučení" našeptávače: jména použitá v projektu/přílohách, která v číselníku