SKILL: Uporządkowanie skills domenowych - podział na mniejsze pliki i wspólna numeracja

This commit is contained in:
Marcin Wojas
2026-06-07 08:31:37 +02:00
parent fb2f2695a3
commit 5605ad2915
42 changed files with 13500 additions and 13526 deletions
@@ -0,0 +1,348 @@
# KADRY05 — Plan pracy i kalendarz
> Wspólne fakty o typie, podstawowe typy i szablon wzorca: [../kadry.md](../kadry.md).
> **Model kalendarza pracownika.** Każdy `Pracownik` ma kalendarz roboczy
> (`pracownik.Etat.Kalendarz : Soneta.Kalend.Kalendarz`), którego dni leżą w tabeli
> `DniKalendarza` (`DzienKalendarzaBase`, child kalendarza). Pracownik wystawia trzy
> niezależne kolekcje dni typu `DateSubTable` (indeksator po dacie `[Date]`, **tylko do
> odczytu** — element tworzysz konstruktorem + `AddRow`):
> - `pracownik.DniPlanu : DateSubTable` — **plan/harmonogram** (dni `DzienPlanu : DzienKalendarzaBase`); to `pracownik.Etat.Kalendarz.Dni`.
> - `pracownik.DniPracy : DateSubTable<Soneta.Kalend.DzienPracy>` — **ewidencja** (realizacja) czasu pracy.
> - `pracownik.DniRCP : DateSubTable<Soneta.Kalend.DzienRCP>` — **zarejestrowany** czas pracy (RCP) — patrz sekcja F.
>
> Wszystkie dni współdzielą subrow `Praca : Soneta.Kalend.CzasPracy` z polami
> `OdGodziny`/`DoGodziny`/`Czas : Soneta.Types.Time`. Definicja dnia (`Definicja :
> Soneta.Kalend.DefinicjaDnia`) to rekord **konfiguracyjny** (słownik `DefinicjeDni`,
> indeksator `[Kod]`).
>
> **Ograniczenie wykonalności.** Plan i ewidencja są normalnie wyliczane przez kalkulator
> czasu pracy z definicji kalendarza/serii — ręczne tworzenie pojedynczego dnia jest możliwe
> publicznym kontraktem (ctor `(Pracownik, Date)` + `AddRow`), ale **wymaga zdefiniowanego
> `DefinicjaDnia` w konfiguracji**. Operacje masowe (przeliczenie planu na okres) są zaszyte
> w workerach/kalkulatorach UI — patrz KADRY-E2.
### KADRY-E1 — Wprowadzanie planowanego czasu pracy (★)
**Cel:** odczytać lub ustawić plan pracy (harmonogram) pracownika na konkretny dzień —
godziny oddo, normę dobową oraz typ dnia.
**Pola i typy:**
| Element | Lokalizacja | Typ | Uwaga |
|---|---|---|---|
| Plan pracy (cała kolekcja) | `pracownik.DniPlanu` | `Soneta.Business.DateSubTable` | == `pracownik.Etat.Kalendarz.Dni`; indeksator `[Date]` (get) |
| Dzień planu | `pracownik.DniPlanu[data]` | `Soneta.Kalend.DzienPlanu` (`DzienKalendarzaBase`) | `null`, gdy dla daty brak dnia planu |
| Data dnia | `DzienPlanu.Data` | `Soneta.Types.Date` | bazodanowe; ustawiane przez ctor |
| Godziny pracy (subrow) | `DzienPlanu.Praca` | `Soneta.Kalend.CzasPracy` | `Praca.OdGodziny`, `Praca.DoGodziny`, `Praca.Czas : Time` (zapisywalne) |
| Czas (norma dnia, odczyt) | `DzienPlanu.Czas` | `Soneta.Types.Time` | kalkulowane (czas pracy dnia) |
| Od (odczyt) | `DzienPlanu.OdGodziny` | `Soneta.Types.Time` | kalkulowane |
| Definicja dnia | `DzienPlanu.Definicja` | `Soneta.Kalend.DefinicjaDnia` | rekord słownika konfiguracyjnego `DefinicjeDni` |
| Tolerancja wejścia | `DzienPlanu.TolerancjaWe` | `Soneta.Types.Time` | bazodanowe |
| Norma dobowa kalendarza | `pracownik.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza, nie dnia |
| Słownik definicji dni | `session.GetKalend().DefinicjeDni` | `DefinicjeDni` | indeksator `[kod: string]`; skróty `WolnaSobota`, `Niedziela` |
**Snippet:**
```csharp
var kalend = session.GetKalend();
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
// --- Odczyt planu na dzień (bezpiecznie: indeksator zwraca null dla braku dnia) ---
var data = new Date(2026, 6, 1);
var dzienPlanu = (DzienPlanu)pracownik.DniPlanu[data];
if (dzienPlanu is not null)
{
Time odGodz = dzienPlanu.Praca.OdGodziny; // np. 8:00
Time doGodz = dzienPlanu.Praca.DoGodziny; // np. 16:00
Time normaDnia = dzienPlanu.Czas; // wyliczona norma dnia (kalkulowane)
DefinicjaDnia typDnia = dzienPlanu.Definicja;
}
// --- Ustawienie/utworzenie dnia planu (wymaga DefinicjaDnia z konfiguracji) ---
using (var t = session.Logout(editMode: true))
{
var dp = (DzienPlanu)pracownik.DniPlanu[data];
if (dp is null)
{
dp = session.AddRow(new DzienPlanu(pracownik, data)); // ctor (Pracownik, Date)
dp.Definicja = kalend.DefinicjeDni["RB"]; // typ dnia ze słownika (np. dzień roboczy)
}
dp.Praca.OdGodziny = new Time(8, 0);
dp.Praca.DoGodziny = new Time(16, 0); // Czas dnia wylicza się z oddo
t.Commit();
}
session.Save();
```
**Pułapki:**
- `DniPlanu` to `DateSubTable` **nietypowany** (zwraca `Row`) — rzutuj na `DzienPlanu`. Indeksator
`[Date]` jest **tylko do odczytu**: nowego dnia nie „przypiszesz", tworzysz go ctorem
`new DzienPlanu(pracownik, data)` + `session.AddRow(...)`.
- Godziny ustawiasz na **subrowie** `Praca` (`dp.Praca.OdGodziny = …`), nie na `dp.OdGodziny`
to ostatnie jest kalkulowane (read-only). Po ustawieniu oddo `Praca.Czas`/`Czas` przeliczają się.
- `Definicja` to rekord **konfiguracyjnego** słownika `DefinicjeDni` — pobierz istniejący wpis
(`kalend.DefinicjeDni[kod]`), nie twórz „w locie". Bez przypisanego `Definicja` świeży dzień planu
może nie przejść weryfikatorów.
- Plan jest zwykle generowany przez kalkulator z definicji kalendarza (serie dni, święta) —
ręczne nadpisywanie pojedynczego dnia to korekta, nie sposób budowy całego harmonogramu (do tego
służy operacja seryjna / kopiowanie planu, KADRY-E2).
- Norma dobowa to atrybut **kalendarza** (`Etat.Kalendarz.NormaDobowa`), nie pojedynczego dnia.
### KADRY-E2 — Planowanie czasu pracy grupy (kopiowanie planu) (★)
**Cel:** skopiować wyliczony plan pracy (harmonogram) na wskazany okres — dla jednego pracownika
albo dla grupy, oraz seryjnie zaktualizować kalendarz pracowników (zmiana kalendarza docelowego).
**Publiczny kontrakt — dwie drogi:**
| Operacja | API | Charakter |
|---|---|---|
| Kopiowanie **planu** pracownika na okres | `Soneta.Kalend.KalendarzPlanuKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API |
| Kopiowanie **pracy/realizacji** na okres | `Soneta.Kalend.KalendarzPracyKopia.Kopiuj(Pracownik pracownik, FromTo okres)` (**public static**) | bez UI — proste API |
| Kopiowanie grupy (worker UI) | `KalendarzPlanuKopia.KopiujWorker` / `KalendarzPracyKopia.KopiujWorker` | wymaga `Context` z zaznaczeniem |
| Aktualizacja kalendarza grupy | `Soneta.Kadry.AktualizujKalendarzWorker` | wymaga `Params` z `Context` |
**Worker `KopiujWorker` (BI/„Kopiuj plan…", „Kopiuj pracę…"):** klasa `ContextBase` z ctorem
`(Context context)`; pola `[Context] FromTo Okres`, `[Context] Pracownik[] Pracownicy`; metoda
`void Kopiuj()`. Działa **wyłącznie** z kontekstem UI (zaznaczona lista pracowników) i jest gardzona
licencją BI/BI_PL/PL oraz `IsVisibleKopiuj` (niedostępny na mobile).
**Worker `AktualizujKalendarzWorker`:** pola `[Context] Pracownik[] Pracownicy`,
`Params Pars` (`Pars.Data`, `Pars.TylkoOstatni: bool`, `Pars.PowodAktualizacji: string`,
`Pars.Kalendarze: KalendarzBase[]`, `Pars.Docelowy: Kalendarz`, `Pars.Zmiana: bool`,
`Pars.Interpretacja`), metoda `void Aktualizuj()`. `Params` to `ContextBase` (ctor `(Context)`).
**Snippet (proste API dla jednego pracownika — bez UI):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
using (var t = session.Logout(editMode: true))
{
// Wylicza plan z kalendarza i zapisuje do kopii planu pracownika za wskazany okres:
KalendarzPlanuKopia.Kopiuj(pracownik, okres); // public static
// analogicznie realizacja: KalendarzPracyKopia.Kopiuj(pracownik, okres);
t.Commit();
}
session.Save();
```
**Snippet (grupa — przez worker; wymaga Context z zaznaczeniem):**
```csharp
// Tylko w warstwie UI/Czynności — Context dostarcza zaznaczonych pracowników.
var worker = new KalendarzPlanuKopia.KopiujWorker(context)
{
Okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30)),
Pracownicy = context.Get<Pracownik[]>()
};
worker.Kopiuj(); // wewnątrz: Session.Logout + Commit
```
**Pułapki:**
- **Kopiowanie grupy nie ma „czystego" API bezkontekstowego** — `KopiujWorker` i
`AktualizujKalendarzWorker.Params` dziedziczą po `ContextBase` i wymagają `Context` (zaznaczenie z
listy UI). Dla kodu serwerowego/testów używaj **publicznej statycznej** `KalendarzPlanuKopia.Kopiuj(pracownik, okres)`
w pętli po pracownikach — to ona realizuje właściwą logikę (worker w `KopiujInt` woła ją per pracownik).
- `KopiujWorker.Kopiuj()` jest gardzony licencją (BI/BI_PL/PL) i `IsVisibleKopiuj` (m.in. blokada na
mobile) — to logika UI, nie wywołuj jej z kodu biznesowego.
- Kopia planu/pracy trafia do **osobnych** kolekcji `pracownik.DniPlanuKopia`/`pracownik.DniPracyKopia`
(`DateSubTable`), powiązanych z `KalendarzPlanuKopia`/`KalendarzPracyKopia` — to bufor kopii, odrębny
od właściwego `DniPlanu`/`DniPracy`.
- `okres` jest normalizowany przez setter workera do pełnych miesięcy (otwarty `From`/`To`
pierwszy/ostatni dzień miesiąca); przy statycznym `Kopiuj` podawaj zamknięty `FromTo`.
- Operacja seryjna na grupie pracowników = długa transakcja → dziel na paczki, trzymaj transakcje
krótkie (safe-code §13.1).
### KADRY-E3 — Aktualizacja kalendarza pracownika (operacja seryjna „Zaktualizuj kalendarz pracownika")
**Cel:** seryjnie zmienić kalendarz roboczy zaznaczonych pracowników (zmiana kalendarza
docelowego, przeliczenie planu na nowy kalendarz od wskazanej daty) — operacja z menu
„Czynności" na liście pracowników.
**Publiczny kontrakt — worker `Soneta.Kadry.AktualizujKalendarzWorker`:**
| Element | Sygnatura / typ | Uwaga |
|---|---|---|
| Konstruktor | `new AktualizujKalendarzWorker()` | bezparametrowy; worker UI |
| Pracownicy (wejście) | `Pracownicy : Pracownik[]` | **set-only**; karmione z `Context` (zaznaczenie listy) |
| Parametry | `Pars : Params` | **set-only**; `Params` to `ContextBase`, ctor `(Context context)` |
| Wykonanie | `void Aktualizuj()` | właściwa operacja seryjna (Logout + Commit wewnątrz) |
**`Soneta.Kadry.AktualizujKalendarzWorker.Params` (`: ContextBase`, ctor `(Context)`):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Data` | `Soneta.Types.Date` | data, od której obowiązuje nowy kalendarz |
| `TylkoOstatni` | `bool` | aktualizuj tylko ostatni (bieżący) zapis historyczny |
| `PowodAktualizacji` | `string` | opis powodu (do dokumentu aktualizacji) |
| `Kalendarze` | `KalendarzBase[]` | kalendarze źródłowe objęte zmianą; lista przez `GetListKalendarze()` |
| `Docelowy` | `Soneta.Kalend.Kalendarz` | kalendarz docelowy; lista przez `GetListDocelowy()` |
| `Zmiana` | `bool` | flaga: czy zmienić kalendarz (a nie tylko przeliczyć) |
| `Interpretacja` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu` / `WgObecnosci` / `WgZestawien`; `IsReadOnlyInterpretacja()` |
**Snippet (warstwa UI/Czynności — wymaga `Context` z zaznaczeniem):**
```csharp
// Tylko w warstwie UI: Context dostarcza zaznaczonych pracowników.
var worker = new AktualizujKalendarzWorker
{
Pracownicy = context.Get<Pracownik[]>(),
Pars = new AktualizujKalendarzWorker.Params(context)
{
Data = new Date(2026, 7, 1),
Docelowy = session.GetKalend().Kalendarze.WgKodu["PODSTAWOWY"],
Zmiana = true,
Interpretacja = InterpretacjaKalendarza.WgPlanu,
PowodAktualizacji = "Zmiana systemu czasu pracy"
}
};
worker.Aktualizuj(); // wewnątrz: Session.Logout + Commit
```
**Pułapki:**
- `Params` dziedziczy po `ContextBase` (ctor `(Context)`) — **nie da się go zbudować bez `Context`**.
Dlatego KADRY-E3 nie ma „czystego" API bezkontekstowego; to operacja UI/serwerowa z zaznaczeniem.
- `Pracownicy` i `Pars`**set-only** — nie odczytasz ich z powrotem; ustaw przed `Aktualizuj()`.
- Operacja seryjna = długa transakcja na wielu pracownikach → w realnym użyciu dziel na paczki
(safe-code §13.1). Sam worker zarządza transakcją wewnętrznie.
- Zmiana kalendarza jest **historyczna** (operuje na zapisach `Etat`) — `TylkoOstatni`/`Data`
decydują, których zapisów historycznych dotyczy.
---
### KADRY-E4 — Uzgodnienie doby pracowniczej (model doby; godziny rozpoczęcia doby)
**Cel:** przesunąć granicę doby pracowniczej dla dnia ewidencji — gdy zmiana zaczyna się w jednej
dobie kalendarzowej, a kończy w następnej (nocna), uzgodnienie „przenosi" początek/koniec pracy do
właściwej doby pracowniczej. Operacja na pojedynczym dniu (`DzienPracy`) lub seryjnie na grupie.
**Model doby (publiczny kontrakt):**
| Element | Lokalizacja | Typ | Uwaga |
|---|---|---|---|
| Początek doby w niedziele/święta | `pracownik.Last.Etat.ConfigPoczątekDobyNiedzieledIŚwięta` | `Soneta.Types.Time` | **read-only** (konfiguracyjne); godzina startu doby |
| Norma dobowa | `pracownik.Last.Etat.NormaDobowa` | `Soneta.Types.Time` | bazodanowe; norma czasu doby |
| Norma dobowa kalendarza | `pracownik.Last.Etat.Kalendarz.NormaDobowa` | `Soneta.Types.Time` | poziom kalendarza |
| Interpretacja kalendarza | `pracownik.Last.Etat.InterpretacjaKalendarza` | `Soneta.Kadry.InterpretacjaKalendarza` | `WgPlanu`/`WgObecnosci`/`WgZestawien` — jak interpretować dobę |
> **Uwaga:** `Etat` leży na bieżącym **zapisie historycznym** (`pracownik.Last.Etat : Soneta.Kadry.Etat`,
> gdzie `Last : PracHistoria`) — nie ma property `pracownik.Etat` bezpośrednio na roocie pracownika.
| Godziny pracy dnia | `DzienPracy.Praca` | `Soneta.Kalend.CzasPracy` | `OdGodziny`/`DoGodziny`/`Czas` — granice realizacji w dobie |
**Worker pojedynczego dnia — `Soneta.Kalend.DzienPracy.UzgodnijDobePracowniczaWorker`:**
| Element | Sygnatura | Uwaga |
|---|---|---|
| Konstruktor | `new DzienPracy.UzgodnijDobePracowniczaWorker()` | |
| Dzień (wejście) | `Dzień : DzienPracy` | **set-only** |
| Warunek dostępności | `static bool IsEnabledUzgodnijDobePracownicza(DzienPracy dzień)` | czy operacja ma sens dla dnia |
| Uzgodnienie | `object UzgodnijDobePracownicza()` | przelicza dobę |
| Przeniesienie początku | `DzienPracy PrzenieśPoczątek()` | przenosi początek pracy do poprz. doby |
| Przeniesienie końca | `DzienPracy PrzenieśKoniec()` | przenosi koniec pracy do nast. doby |
| Dokument aktualizacji | `DokumentAktualizacjiKalendarza : IDokumentAktualizacjiKalendarza`, `DataAktualizacji : System.DateTime` | kontekst historii |
**Worker seryjny (grupa) — `Soneta.Kadry.UzgodnijDobePracowniczaPracownikowWorker`:**
| Element | Sygnatura / typ | Uwaga |
|---|---|---|
| Konstruktor | `new UzgodnijDobePracowniczaPracownikowWorker()` | |
| Pracownicy | `Pracownicy : Pracownik[]` | **set-only**; z `Context` |
| Parametry | `Pars : Params` (`ContextBase`, ctor `(Context)`); pole `Okres : FromTo` | **set-only** |
| Wykonanie | `UzgodnijDobePracowniczaResult UzgodnijDobePracownicza()` | zwraca wynik |
**Snippet (pojedynczy dzień):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var dzien = pracownik.DniPracy[new Date(2026, 6, 1)]; // DzienPracy lub null
if (dzien is not null && DzienPracy.UzgodnijDobePracowniczaWorker.IsEnabledUzgodnijDobePracownicza(dzien))
{
using (var t = session.Logout(editMode: true))
{
var worker = new DzienPracy.UzgodnijDobePracowniczaWorker { Dzień = dzien };
worker.UzgodnijDobePracownicza();
t.Commit();
}
session.Save();
}
```
**Pułapki:**
- Godzina rozpoczęcia doby to atrybut **konfiguracyjny `Etat`** (`ConfigPoczątekDobyNiedzieledIŚwięta`,
read-only) i normy `Etat.NormaDobowa`/`Etat.Kalendarz.NormaDobowa` — nie ma osobnego, edytowalnego
pola „początek doby" na pojedynczym `DzienPracy`.
- `Dzień` workera pojedynczego jest **set-only**; `Pracownicy`/`Pars` workera grupowego również.
- Worker grupowy `Params` to `ContextBase` (ctor `(Context)`) — **wymaga `Context`** (zaznaczenie UI),
brak czystego API bezkontekstowego.
- Uzgodnienie modyfikuje `DzienPracy.Praca` (oddo) i może rozbić pracę na dwie doby — wykonuj w
transakcji (`Logout(editMode:true)` + `Commit`) i zapisz `Save()`.
---
### KADRY-E5 — Odczyt normy czasu pracy i czasu przepracowanego za okres (★ testowalne)
**Cel:** dla pracownika odczytać za zadany okres (`FromTo`/`YearMonth`): normę czasu pracy
(planowaną), czas przepracowany (zrealizowany), nadgodziny, czas nocny, liczbę/normę nieobecności —
bez modyfikacji danych (czysty odczyt statystyk).
**Punkt wejścia — `pracownik.Czasy : Soneta.Kalend.KalkulatorPracownika`:**
| Metoda (publiczna, instancyjna) | Zwraca | Znaczenie |
|---|---|---|
| `Norma(FromTo okres, params Item[] condition)` | `CzasDni` | norma (planowana) czasu pracy za okres |
| `Norma(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | norma w obrębie strefy |
| `NormaKodeksowa(YearMonth miesiąc)` | `CzasDni` | norma kodeksowa miesiąca (pełny etat) |
| `NormaKodeksowaWym(Fraction wymiar, Time normaDobowa, YearMonth miesiąc)` | `CzasDni` | norma kodeksowa wg wymiaru etatu |
| `Praca(FromTo okres, params Item[] condition)` | `CzasDni` | czas **przepracowany** (zrealizowany) za okres |
| `Praca(FromTo okres, DefinicjaStrefy def, params Item[] condition)` | `CzasDni` | przepracowany w obrębie strefy |
| `PracaRozliczana(FromTo okres, params Item[] condition)` | `CzasDni` | czas pracy rozliczany (do nadgodzin) |
| `PracaZatr(FromTo okres, bool usprPłatne)` | `CzasDni` | praca w okresie zatrudnienia |
| `Nadgodziny(YearMonth okres)` / `Nadgodziny(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny |
| `NadgodzinyDobaOkres(FromTo okres)` | `ZestawienieNadgodzin` | nadgodziny dobowe/okresowe |
| `Nocne(YearMonth\|FromTo okres)` | `Time` | czas nocny |
| `NormaNie(YearMonth\|FromTo okres, params Item[] condition)` | `CzasDni` | norma nieobecności |
| `DniNie(YearMonth\|FromTo okres, params Item[] condition)` | `int` | liczba dni nieobecności |
| `Nieobecność(Date data[, bool clip])` | `INieobecnosc` | nieobecność w danym dniu |
**`Soneta.Kalend.CzasDni` (typ wyniku):**
| Pole | Typ | Uwaga |
|---|---|---|
| `Czas` | `Soneta.Types.Time` | sumaryczny czas (read-only) |
| `Dni` | `int` | liczba dni (read-only) |
| `CzasDni.Empty`, `CzasDni.Invalid` | `CzasDni` | wartości specjalne; operatory `+`/`-`/`==` |
**`Soneta.Kalend.ZestawienieNadgodzin` (struct):** `N50`, `N100`, `NSW`, `N100Doba`, `N100Okres`,
`Razem` — wszystkie `Time` (read-only); `ZestawienieNadgodzin.Zero`.
**Snippet (czysty odczyt):**
```csharp
var pracownik = session.GetKadry().Pracownicy.WgKodu["006"];
var kalk = pracownik.Czasy; // KalkulatorPracownika
var okres = new FromTo(new Date(2026, 6, 1), new Date(2026, 6, 30));
CzasDni norma = kalk.Norma(okres); // norma planowana
CzasDni przepracowano = kalk.Praca(okres); // czas zrealizowany
ZestawienieNadgodzin nadg = kalk.Nadgodziny(new YearMonth(2026, 6));
Time nocne = kalk.Nocne(okres);
Time normaCzas = norma.Czas; int normaDni = norma.Dni;
Time pracaCzas = przepracowano.Czas; Time nadgRazem = nadg.Razem;
```
**Pułapki:**
- `KalkulatorPracownika` **nie jest `Row`** — to obiekt liczący (zwykły `object`). Nie zapisuje się,
nie wymaga transakcji; to czysty odczyt. Pobieraj go zawsze przez `pracownik.Czasy` (ma kontekst
pracownika), nie twórz ręcznie ctorem chyba że masz `Pracownik` + ewentualny `Log`.
- Parametr `condition` to **serwerowy filtr** (`Item[]`, RowCondition) — można zawęzić np. do strefy;
zwykle pusty.
- `Norma` = plan, `Praca` = realizacja; nie myl `Praca(okres)` (statystyka) z `DzienPracy` (rekord dnia).
- Wynik `CzasDni.Invalid` sygnalizuje brak danych/błąd okresu — sprawdzaj zanim policzysz różnice.
---