@hrbolek/uoisfrontend-template 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.js +17 -17
- package/dist/es/index.js +38 -31
- package/dist/umd/index.js +31 -31
- package/package.json +1 -1
- package/src/Base/Components/Link.jsx +13 -8
- package/src/Base/Mutations/General.jsx +3 -1
- package/src/Base/Mutations/Update.jsx +5 -0
- package/src/EventGQLModel/Components/A4Plan/A4Plan.jsx +363 -0
- package/src/EventGQLModel/Components/A4Plan/index.js +1 -0
- package/src/EventGQLModel/Components/CardCapsule.jsx +43 -0
- package/src/EventGQLModel/Components/Children.jsx +31 -0
- package/src/EventGQLModel/Components/ConfirmEdit.jsx +61 -0
- package/src/EventGQLModel/Components/Filter.jsx +14 -0
- package/src/EventGQLModel/Components/LargeCard.jsx +54 -0
- package/src/EventGQLModel/Components/Link.jsx +55 -0
- package/src/EventGQLModel/Components/LiveEdit.jsx +111 -0
- package/src/EventGQLModel/Components/MediumCard.jsx +39 -0
- package/src/EventGQLModel/Components/MediumContent.jsx +96 -0
- package/src/EventGQLModel/Components/MediumEditableContent.jsx +35 -0
- package/src/EventGQLModel/Components/Plan/PlanRow.jsx +470 -0
- package/src/EventGQLModel/Components/Plan/_utils.js +971 -0
- package/src/EventGQLModel/Components/Plan/calendarReducer.js +535 -0
- package/src/EventGQLModel/Components/Plan/index.js +3 -0
- package/src/EventGQLModel/Components/Table.jsx +7 -0
- package/src/EventGQLModel/Components/index.js +15 -0
- package/src/EventGQLModel/Mutations/Create.jsx +202 -0
- package/src/EventGQLModel/Mutations/Delete.jsx +173 -0
- package/src/EventGQLModel/Mutations/InteractiveMutations.jsx +30 -0
- package/src/EventGQLModel/Mutations/Update.jsx +147 -0
- package/src/EventGQLModel/Mutations/helpers.jsx +7 -0
- package/src/EventGQLModel/Pages/PageBase.jsx +56 -0
- package/src/EventGQLModel/Pages/PageCreateItem.jsx +28 -0
- package/src/EventGQLModel/Pages/PageDeleteItem.jsx +16 -0
- package/src/EventGQLModel/Pages/PageNavbar.jsx +160 -0
- package/src/EventGQLModel/Pages/PagePlan.jsx +42 -0
- package/src/EventGQLModel/Pages/PageReadItem.jsx +11 -0
- package/src/EventGQLModel/Pages/PageReadItemEx.jsx +42 -0
- package/src/EventGQLModel/Pages/PageSubevents.jsx +43 -0
- package/src/EventGQLModel/Pages/PageUpdateItem.jsx +14 -0
- package/src/EventGQLModel/Pages/PageVector.jsx +80 -0
- package/src/EventGQLModel/Pages/RouterSegment.jsx +82 -0
- package/src/EventGQLModel/Pages/index.js +2 -0
- package/src/EventGQLModel/Queries/DeleteAsyncAction.jsx +32 -0
- package/src/EventGQLModel/Queries/Fragments.jsx +123 -0
- package/src/EventGQLModel/Queries/InsertAsyncAction.jsx +40 -0
- package/src/EventGQLModel/Queries/ReadAsyncAction.jsx +44 -0
- package/src/EventGQLModel/Queries/ReadPageAsyncAction.jsx +13 -0
- package/src/EventGQLModel/Queries/ReadSubEventsAsyncAction.jsx +44 -0
- package/src/EventGQLModel/Queries/SearchAsyncAction.jsx +16 -0
- package/src/EventGQLModel/Queries/UpdateAsyncAction.jsx +40 -0
- package/src/EventGQLModel/Queries/index.js +6 -0
- package/src/EventGQLModel/Scalars/ScalarAttribute.jsx +54 -0
- package/src/EventGQLModel/Scalars/TemplateScalarAttribute.jsx +88 -0
- package/src/EventGQLModel/Scalars/index.js +1 -0
- package/src/EventGQLModel/Vectors/TemplateVectorsAttribute.jsx +326 -0
- package/src/EventGQLModel/Vectors/VectorAttribute.jsx +56 -0
- package/src/EventGQLModel/Vectors/index.js +1 -0
- package/src/EventGQLModel/WhatToDo.md +44 -0
- package/src/EventGQLModel/index.js +71 -0
- package/src/GroupGQLModel/Mutations/Create.jsx +8 -2
- package/src/GroupGQLModel/Mutations/Delete.jsx +8 -2
- package/src/GroupGQLModel/Mutations/Update.jsx +8 -8
- package/src/GroupGQLModel/Queries/Fragments.jsx +17 -1
- package/src/GroupGQLModel/Scalars/RBACObject.jsx +17 -5
- package/src/GroupGQLModel/Vectors/GroupMemberships.jsx +1 -1
- package/src/UserGQLModel/Components/MediumContent.jsx +9 -3
- package/src/UserGQLModel/Queries/Fragments.jsx +6 -0
- package/src/_Template/WhatToDo.md +1 -1
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import { Strava } from "react-bootstrap-icons";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
const GREY = "#777777";
|
|
5
|
+
|
|
6
|
+
/* =========================
|
|
7
|
+
TOOLS
|
|
8
|
+
========================= */
|
|
9
|
+
|
|
10
|
+
const tools = [
|
|
11
|
+
{ id: "ac3238a2-a3ca-4f4b-a56b-8ac7c3953aff", name: "výuka - zimní semestr", abbreviation: "ZS", color: "#ffffff" },
|
|
12
|
+
{ id: "78a6f015-b8f4-49c8-b218-7861454cb8e9", name: "výuka - letní semestr", abbreviation: "LS", color: "#f2f2f2" },
|
|
13
|
+
|
|
14
|
+
{ id: "6fef77f1-a580-4e13-a088-30368a95af2f", name: "řádná dovolená", abbreviation: "ŘD", color: "#d9a441" },
|
|
15
|
+
{ id: "7e4187e5-b219-4332-b50e-54411541bba6", name: "zkouškové období", abbreviation: "Z", color: "#f28c28" },
|
|
16
|
+
{ id: "79c71457-b926-449a-b227-a3461bf1df4c", name: "příprava v poli - teorie", abbreviation: "PT", color: "#8bc34a" },
|
|
17
|
+
{ id: "539c45b2-629c-43ca-87cb-db753e7bbab9", name: "příprava v poli - praxe", abbreviation: "PP", color: "#7cb342" },
|
|
18
|
+
{ id: "83d0a942-6456-4f35-bce8-37c9a08cb966", name: "rezerva", abbreviation: "R", color: "#ff1f1f" },
|
|
19
|
+
|
|
20
|
+
{ id: "5f147c3c-b307-4334-a339-6f4c83f1dad7", name: "intenzivní kurz AJ", abbreviation: "AJ", color: "#ffff33" },
|
|
21
|
+
{ id: "48ff1e53-83c8-48d6-84e6-b63e4f5fa60d", name: "kurz TV", abbreviation: "TV", color: "#7a6000" },
|
|
22
|
+
{ id: "1af8ea7e-b280-4202-8efe-1df22763081e", name: "odborná praxe", abbreviation: "OP", color: "#bdbdbd" },
|
|
23
|
+
{ id: "0b624b6c-ed72-4316-b070-8e09f73e531c", name: "stáž na systematizovaném místě", abbreviation: "SSM", color: "#d9d9d9" },
|
|
24
|
+
{ id: "cd309340-cae0-4ec2-8adc-f4c61dc5f023", name: "letecká AJ", abbreviation: "LA", color: "#ffff66" },
|
|
25
|
+
{ id: "0f5713ca-7134-4c6a-99a0-7166c689dbfb", name: "vyřazení", abbreviation: "V", color: "#ff1f1f" },
|
|
26
|
+
{ id: "7edccbad-3c70-49ec-b5ca-b744bec8d234", name: "aplikované vojenské technologie", abbreviation: "AVT", color: "#00b0f0" },
|
|
27
|
+
|
|
28
|
+
{ id: "d9494b47-b46c-4ba4-a900-1443751cabce", name: "příprava na SZZ", abbreviation: "PSZZ", color: "#a64ac9" },
|
|
29
|
+
{ id: "eb86a21b-04e2-419c-a3a3-5f21603d349b", name: "SZZ", abbreviation: "SZZ", color: "#8e24aa" },
|
|
30
|
+
{ id: "4d0d3e3f-4d45-4fc4-95f5-612905cf5823", name: "kurz SERE, případně LV, CANI, PARA", abbreviation: "SERE", color: "#9fd3f2" },
|
|
31
|
+
{ id: "4560fca2-533b-45c1-945c-16dcf0935126", name: "zkouškové období / letecký výcvik", abbreviation: "ŘD/LV", color: "#d4af37" },
|
|
32
|
+
{ id: "3d5c8255-9abc-4d3e-aa25-b3984e5ce1a1", name: "diplomový projekt", abbreviation: "DP", color: "#d9d9d9" }
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Vytvoří mapu nástrojů podle jejich zkratky (abbreviation).
|
|
37
|
+
*
|
|
38
|
+
* Výstupem je objekt, kde klíčem je `tool.abbreviation`
|
|
39
|
+
* a hodnotou je celý objekt nástroje.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array<{ id: string, name: string, abbreviation: string, color: string }>} tools
|
|
42
|
+
* Seznam nástrojů.
|
|
43
|
+
*
|
|
44
|
+
* @returns {{ [abbreviation: string]: { id: string, name: string, abbreviation: string, color: string } }}
|
|
45
|
+
* Objektová mapa nástrojů indexovaná podle zkratky.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* const tools = [
|
|
49
|
+
* { id: "1", name: "výuka - zimní semestr", abbreviation: "ZS", color: "#ffffff" },
|
|
50
|
+
* { id: "2", name: "zkouškové období", abbreviation: "Z", color: "#f28c28" }
|
|
51
|
+
* ];
|
|
52
|
+
*
|
|
53
|
+
* const toolMap = createToolMap(tools);
|
|
54
|
+
*
|
|
55
|
+
* console.log(toolMap["ZS"].name); // "výuka - zimní semestr"
|
|
56
|
+
*/
|
|
57
|
+
export function createToolMap(tools) {
|
|
58
|
+
return Object.fromEntries(tools.map(tool => [tool.abbreviation, tool]));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Vrátí kontrastní barvu textu (#000 nebo #fff) pro danou barvu pozadí.
|
|
63
|
+
*
|
|
64
|
+
* Používá YIQ algoritmus pro odhad světlosti barvy.
|
|
65
|
+
* Pokud je pozadí světlé → vrací černý text (#000),
|
|
66
|
+
* pokud je tmavé → vrací bílý text (#fff).
|
|
67
|
+
*
|
|
68
|
+
* Podporuje hex formáty:
|
|
69
|
+
* - #RGB (např. #fff)
|
|
70
|
+
* - #RRGGBB (např. #ffffff)
|
|
71
|
+
*
|
|
72
|
+
* Pokud vstup není validní hex barva, vrací výchozí #000.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} bgColor Barva pozadí ve formátu hex (#RGB nebo #RRGGBB).
|
|
75
|
+
* @returns {"#000" | "#fff"} Doporučená barva textu pro dostatečný kontrast.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* getContrastTextColor("#ffffff") // "#000"
|
|
79
|
+
* getContrastTextColor("#000000") // "#fff"
|
|
80
|
+
* getContrastTextColor("#ff0000") // "#fff"
|
|
81
|
+
* getContrastTextColor("#eee") // "#000"
|
|
82
|
+
*/
|
|
83
|
+
export function getContrastTextColor(bgColor) {
|
|
84
|
+
if (!bgColor || bgColor[0] !== "#") return "#000";
|
|
85
|
+
let hex = bgColor.replace("#", "");
|
|
86
|
+
if (hex.length === 3) {
|
|
87
|
+
hex = hex.split("").map(ch => ch + ch).join("");
|
|
88
|
+
}
|
|
89
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
90
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
91
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
92
|
+
const yiq = (r * 299 + g * 587 + b * 114) / 1000;
|
|
93
|
+
return yiq >= 160 ? "#000" : "#fff";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* =========================
|
|
97
|
+
DATE / ISO HELPERS
|
|
98
|
+
========================= */
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Převede vstup (Date | YYYY-MM-DD | ISO datetime string)
|
|
102
|
+
* na lokální Date bez časové složky (00:00:00 local time).
|
|
103
|
+
*
|
|
104
|
+
* @param {Date|string} dateLike
|
|
105
|
+
* @returns {Date}
|
|
106
|
+
*/
|
|
107
|
+
export function toLocalDate(dateLike) {
|
|
108
|
+
if (dateLike instanceof Date) {
|
|
109
|
+
return new Date(
|
|
110
|
+
dateLike.getFullYear(),
|
|
111
|
+
dateLike.getMonth(),
|
|
112
|
+
dateLike.getDate()
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const str = String(dateLike);
|
|
117
|
+
|
|
118
|
+
// vezmi jen datumovou část (před "T")
|
|
119
|
+
const datePart = str.includes("T") ? str.split("T")[0] : str;
|
|
120
|
+
|
|
121
|
+
const [y, m, d] = datePart.split("-").map(Number);
|
|
122
|
+
|
|
123
|
+
return new Date(y, m - 1, d);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Převede Date objekt na string ve formátu ISO `YYYY-MM-DD`.
|
|
128
|
+
*
|
|
129
|
+
* Výstup je vhodný pro:
|
|
130
|
+
* - ukládání dat (např. do JSON)
|
|
131
|
+
* - porovnávání dat jako stringů
|
|
132
|
+
* - další zpracování funkcí jako `toLocalDate`
|
|
133
|
+
*
|
|
134
|
+
* Používá lokální datum (ne UTC), takže nedochází k posunům
|
|
135
|
+
* způsobeným časovým pásmem.
|
|
136
|
+
*
|
|
137
|
+
* @param {Date} date Datum jako JavaScript Date objekt.
|
|
138
|
+
* @returns {string} Datum ve formátu "YYYY-MM-DD".
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* toIsoDate(new Date(2025, 7, 25))
|
|
142
|
+
* // → "2025-08-25"
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* const d = new Date("2025-08-25T15:30:00");
|
|
146
|
+
* toIsoDate(d)
|
|
147
|
+
* // → "2025-08-25"
|
|
148
|
+
*/
|
|
149
|
+
export function toIsoDate(date) {
|
|
150
|
+
const y = date.getFullYear();
|
|
151
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
152
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
153
|
+
return `${y}-${m}-${d}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Naformátuje Date objekt do čitelného stringu ve formátu `DD.MM.YYYY`.
|
|
158
|
+
*
|
|
159
|
+
* Používá lokální datum (ne UTC), takže nedochází k posunům
|
|
160
|
+
* způsobeným časovým pásmem.
|
|
161
|
+
*
|
|
162
|
+
* Vhodné pro:
|
|
163
|
+
* - zobrazení v UI (tooltipy, popisky)
|
|
164
|
+
* - export pro uživatele
|
|
165
|
+
*
|
|
166
|
+
* @param {Date} date Datum jako JavaScript Date objekt.
|
|
167
|
+
* @returns {string} Datum ve formátu "DD.MM.YYYY".
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* formatDate(new Date(2025, 7, 25))
|
|
171
|
+
* // → "25.08.2025"
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* const d = new Date("2025-08-25T15:30:00");
|
|
175
|
+
* formatDate(d)
|
|
176
|
+
* // → "25.08.2025"
|
|
177
|
+
*/
|
|
178
|
+
export function formatDate(date) {
|
|
179
|
+
const y = date.getFullYear();
|
|
180
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
181
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
182
|
+
return `${d}.${m}.${y}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Naformátuje týden do víceřádkového textu ve formátu:
|
|
187
|
+
*
|
|
188
|
+
* DD.MM.
|
|
189
|
+
* DD.MM.
|
|
190
|
+
* YYYY
|
|
191
|
+
*
|
|
192
|
+
* Kde:
|
|
193
|
+
* - první řádek = začátek týdne (pondělí / firstHalfStart)
|
|
194
|
+
* - druhý řádek = konec týdne (neděle / secondHalfEnd)
|
|
195
|
+
* - třetí řádek = rok (podle konce týdne)
|
|
196
|
+
*
|
|
197
|
+
* Používá lokální datum (ne UTC).
|
|
198
|
+
*
|
|
199
|
+
* Vhodné pro:
|
|
200
|
+
* - zobrazení v hlavičce planneru
|
|
201
|
+
* - kompaktní popis týdne (např. v tooltipu nebo labelu)
|
|
202
|
+
*
|
|
203
|
+
* @param {{
|
|
204
|
+
* firstHalfStart: Date,
|
|
205
|
+
* secondHalfEnd: Date
|
|
206
|
+
* }} week Objekt týdne obsahující minimálně začátek a konec týdne.
|
|
207
|
+
*
|
|
208
|
+
* @returns {string} Víceřádkový string s formátovaným týdnem.
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* formatWeek({
|
|
212
|
+
* firstHalfStart: new Date(2025, 7, 25),
|
|
213
|
+
* secondHalfEnd: new Date(2025, 7, 31)
|
|
214
|
+
* })
|
|
215
|
+
* // →
|
|
216
|
+
* // "25.08.
|
|
217
|
+
* // 31.08.
|
|
218
|
+
* // 2025"
|
|
219
|
+
*/
|
|
220
|
+
export function formatWeek(week) {
|
|
221
|
+
const date = week.firstHalfStart
|
|
222
|
+
const date2 = week.secondHalfEnd
|
|
223
|
+
const y = date2.getFullYear();
|
|
224
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
225
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
226
|
+
const m2 = String(date2.getMonth() + 1).padStart(2, "0");
|
|
227
|
+
const d2 = String(date2.getDate()).padStart(2, "0");
|
|
228
|
+
return `${d}.${m}.\n${d2}.${m2}.\n${y}`;
|
|
229
|
+
}
|
|
230
|
+
// {formatDate(week.firstHalfStart)}<br />
|
|
231
|
+
// {formatDate(week.secondHalfEnd)}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Vrátí nový Date objekt posunutý o zadaný počet dní.
|
|
235
|
+
*
|
|
236
|
+
* Zachovává pouze datum (rok, měsíc, den) a ignoruje časovou složku.
|
|
237
|
+
* Výsledný Date je vytvořen v lokálním časovém pásmu, takže nedochází
|
|
238
|
+
* k problémům s UTC posuny (např. při práci s ISO týdny).
|
|
239
|
+
*
|
|
240
|
+
* @param {Date} date Výchozí datum.
|
|
241
|
+
* @param {number} days Počet dní k přičtení (může být i záporný).
|
|
242
|
+
* @returns {Date} Nový Date objekt posunutý o daný počet dní.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* addDays(new Date(2025, 7, 25), 3)
|
|
246
|
+
* // → 28.08.2025
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* addDays(new Date(2025, 7, 25), -1)
|
|
250
|
+
* // → 24.08.2025
|
|
251
|
+
*/
|
|
252
|
+
export function addDays(date, days) {
|
|
253
|
+
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
254
|
+
d.setDate(d.getDate() + days);
|
|
255
|
+
return d;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Vrátí den v týdnu podle ISO standardu (1 = pondělí, 7 = neděle).
|
|
260
|
+
*
|
|
261
|
+
* Oproti JavaScript `Date.getDay()`:
|
|
262
|
+
* - JS: 0 = neděle, 6 = sobota
|
|
263
|
+
* - ISO: 1 = pondělí, 7 = neděle
|
|
264
|
+
*
|
|
265
|
+
* @param {Date} date Datum.
|
|
266
|
+
* @returns {number} ISO den v týdnu (1–7).
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* getISOWeekday(new Date(2025, 7, 25)) // pondělí → 1
|
|
270
|
+
*/
|
|
271
|
+
export function getISOWeekday(date) {
|
|
272
|
+
const day = date.getDay();
|
|
273
|
+
return day === 0 ? 7 : day; // Po=1 ... Ne=7
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Vrátí číslo ISO týdne pro dané datum.
|
|
278
|
+
*
|
|
279
|
+
* ISO týdny:
|
|
280
|
+
* - začínají pondělím
|
|
281
|
+
* - týden 1 je ten, který obsahuje 4. leden
|
|
282
|
+
*
|
|
283
|
+
* Používá UTC výpočty, aby se předešlo problémům s časovým pásmem.
|
|
284
|
+
*
|
|
285
|
+
* @param {Date} date Datum.
|
|
286
|
+
* @returns {number} ISO číslo týdne (1–53).
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* getISOWeek(new Date(2025, 7, 25)) // → např. 35
|
|
290
|
+
*/
|
|
291
|
+
export function getISOWeek(date) {
|
|
292
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
293
|
+
const dayNum = d.getUTCDay() || 7;
|
|
294
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
295
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
296
|
+
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Vrátí ISO rok (week-based year) pro dané datum.
|
|
301
|
+
*
|
|
302
|
+
* Pozor:
|
|
303
|
+
* ISO rok se může lišit od kalendářního roku.
|
|
304
|
+
* Např.:
|
|
305
|
+
* - 1.1. může patřit ještě do posledního ISO týdne předchozího roku
|
|
306
|
+
* - 31.12. může patřit do týdne 1 následujícího roku
|
|
307
|
+
*
|
|
308
|
+
* @param {Date} date Datum.
|
|
309
|
+
* @returns {number} ISO rok.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* getISOWeekYear(new Date(2025, 0, 1)) // může vrátit 2024
|
|
313
|
+
*/
|
|
314
|
+
export function getISOWeekYear(date) {
|
|
315
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
316
|
+
const dayNum = d.getUTCDay() || 7;
|
|
317
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
|
318
|
+
return d.getUTCFullYear();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Vrátí počet ISO týdnů v daném roce (52 nebo 53).
|
|
323
|
+
*
|
|
324
|
+
* ISO rok má 53 týdnů pokud:
|
|
325
|
+
* - rok začíná ve čtvrtek
|
|
326
|
+
* - nebo je přestupný a začíná ve středu
|
|
327
|
+
*
|
|
328
|
+
* Implementace využívá fakt, že 28. prosinec
|
|
329
|
+
* vždy spadá do posledního ISO týdne roku.
|
|
330
|
+
*
|
|
331
|
+
* @param {number} year Kalendářní rok.
|
|
332
|
+
* @returns {number} Počet ISO týdnů (52 nebo 53).
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* getISOWeeksInYear(2025) // → 52 nebo 53
|
|
336
|
+
*/
|
|
337
|
+
export function getISOWeeksInYear(year) {
|
|
338
|
+
const dec28 = new Date(year, 11, 28);
|
|
339
|
+
return getISOWeek(dec28);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Vrátí konkrétní datum podle ISO roku, týdne a dne v týdnu.
|
|
344
|
+
*
|
|
345
|
+
* @param {number} year ISO rok.
|
|
346
|
+
* @param {number} week ISO týden (1–53).
|
|
347
|
+
* @param {number} [isoDay=1] ISO den v týdnu (1 = pondělí, 7 = neděle).
|
|
348
|
+
* @returns {Date} Datum odpovídající danému ISO týdnu a dni.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* getDateFromISOWeek(2025, 35, 1) // pondělí 35. týdne
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* getDateFromISOWeek(2025, 35, 7) // neděle 35. týdne
|
|
355
|
+
*/
|
|
356
|
+
export function getDateFromISOWeek(year, week, isoDay = 1) {
|
|
357
|
+
const jan4 = new Date(year, 0, 4);
|
|
358
|
+
const jan4IsoDay = getISOWeekday(jan4);
|
|
359
|
+
const mondayOfWeek1 = addDays(jan4, 1 - jan4IsoDay);
|
|
360
|
+
return addDays(mondayOfWeek1, (week - 1) * 7 + (isoDay - 1));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
/* =========================
|
|
365
|
+
ACADEMIC WEEKS
|
|
366
|
+
========================= */
|
|
367
|
+
/**
|
|
368
|
+
* Vygeneruje seznam týdnů pro akademický rok ve formátu ISO týdnů.
|
|
369
|
+
*
|
|
370
|
+
* Akademický rok:
|
|
371
|
+
* - začíná zadaným ISO týdnem (např. 35) v `startYear`
|
|
372
|
+
* - pokračuje do konce ISO týdnů daného roku
|
|
373
|
+
* - a končí zadaným týdnem (např. 42) v následujícím roce
|
|
374
|
+
*
|
|
375
|
+
* Každý týden obsahuje:
|
|
376
|
+
* - základní ISO identifikaci (rok, číslo týdne)
|
|
377
|
+
* - datumový rozsah (pondělí–neděle)
|
|
378
|
+
* - rozdělení na dvě poloviny týdne:
|
|
379
|
+
* - první polovina: pondělí–středa
|
|
380
|
+
* - druhá polovina: čtvrtek–neděle
|
|
381
|
+
*
|
|
382
|
+
* @param {number} startYear Počáteční rok akademického období.
|
|
383
|
+
* @param {number} [startWeek=35] ISO týden, kterým akademický rok začíná.
|
|
384
|
+
* @param {number} [endWeekNextYear=42] ISO týden v následujícím roce, kterým akademický rok končí.
|
|
385
|
+
*
|
|
386
|
+
* @returns {Array<{
|
|
387
|
+
* id: string,
|
|
388
|
+
* isoYear: number,
|
|
389
|
+
* isoWeek: number,
|
|
390
|
+
* label: string,
|
|
391
|
+
* labelFull: string,
|
|
392
|
+
* weekStart: Date,
|
|
393
|
+
* weekEnd: Date,
|
|
394
|
+
* firstHalfStart: Date,
|
|
395
|
+
* firstHalfEnd: Date,
|
|
396
|
+
* secondHalfStart: Date,
|
|
397
|
+
* secondHalfEnd: Date
|
|
398
|
+
* }>} Seznam týdnů v akademickém roce.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* buildAcademicWeeks(2025, 35, 42)
|
|
402
|
+
* // → týdny od W35/2025 až do W42/2026
|
|
403
|
+
*
|
|
404
|
+
* @example
|
|
405
|
+
* const weeks = buildAcademicWeeks(2025);
|
|
406
|
+
* console.log(weeks[0].labelFull) // "2025/W35"
|
|
407
|
+
* console.log(weeks.at(-1).labelFull) // "2026/W42"
|
|
408
|
+
*/
|
|
409
|
+
export function buildAcademicWeeks(startYear, startWeek = 35, endWeekNextYear = 42) {
|
|
410
|
+
const result = [];
|
|
411
|
+
|
|
412
|
+
const weeksInStartYear = getISOWeeksInYear(startYear);
|
|
413
|
+
for (let w = startWeek; w <= weeksInStartYear; w++) {
|
|
414
|
+
const monday = getDateFromISOWeek(startYear, w, 1);
|
|
415
|
+
const sunday = getDateFromISOWeek(startYear, w, 7);
|
|
416
|
+
result.push({
|
|
417
|
+
id: `${startYear}-W${String(w).padStart(2, "0")}`,
|
|
418
|
+
isoYear: startYear,
|
|
419
|
+
isoWeek: w,
|
|
420
|
+
label: `${w}`,
|
|
421
|
+
labelFull: `${startYear}/W${w}`,
|
|
422
|
+
weekStart: monday,
|
|
423
|
+
weekEnd: sunday,
|
|
424
|
+
firstHalfStart: monday,
|
|
425
|
+
firstHalfEnd: addDays(monday, 2), // Po-St
|
|
426
|
+
secondHalfStart: addDays(monday, 3), // Čt
|
|
427
|
+
secondHalfEnd: sunday // Čt-Ne
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for (let w = 1; w <= endWeekNextYear; w++) {
|
|
432
|
+
const monday = getDateFromISOWeek(startYear + 1, w, 1);
|
|
433
|
+
const sunday = getDateFromISOWeek(startYear + 1, w, 7);
|
|
434
|
+
result.push({
|
|
435
|
+
id: `${startYear + 1}-W${String(w).padStart(2, "0")}`,
|
|
436
|
+
isoYear: startYear + 1,
|
|
437
|
+
isoWeek: w,
|
|
438
|
+
label: `${w}`,
|
|
439
|
+
labelFull: `${startYear + 1}/W${w}`,
|
|
440
|
+
weekStart: monday,
|
|
441
|
+
weekEnd: sunday,
|
|
442
|
+
firstHalfStart: monday,
|
|
443
|
+
firstHalfEnd: addDays(monday, 2),
|
|
444
|
+
secondHalfStart: addDays(monday, 3),
|
|
445
|
+
secondHalfEnd: sunday
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
export function assignEventsToWeeks(weeks, events) {
|
|
454
|
+
const result = weeks.map(week => {
|
|
455
|
+
return {
|
|
456
|
+
...week,
|
|
457
|
+
firstHalfEvents: events.filter(
|
|
458
|
+
event => rangesIntersect(
|
|
459
|
+
week.firstHalfStart,
|
|
460
|
+
week.firstHalfEnd,
|
|
461
|
+
event.startDateObj,
|
|
462
|
+
event.endDateObj
|
|
463
|
+
)
|
|
464
|
+
),
|
|
465
|
+
secondHalfEvents: events.filter(
|
|
466
|
+
event => rangesIntersect(
|
|
467
|
+
week.secondHalfStart,
|
|
468
|
+
week.secondHalfEnd,
|
|
469
|
+
event.startDateObj,
|
|
470
|
+
event.endDateObj
|
|
471
|
+
)
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
return result
|
|
476
|
+
}
|
|
477
|
+
/* =========================
|
|
478
|
+
EVENTS <-> SEGMENTS
|
|
479
|
+
========================= */
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Ověří, zda se dva časové intervaly překrývají (včetně hranic).
|
|
483
|
+
*
|
|
484
|
+
* Intervaly jsou považovány za uzavřené:
|
|
485
|
+
* - [aStart, aEnd]
|
|
486
|
+
* - [bStart, bEnd]
|
|
487
|
+
*
|
|
488
|
+
* To znamená, že pokud se dotýkají na hraně, považují se za průnik:
|
|
489
|
+
* např. [1.1, 3.1] a [3.1, 5.1] → TRUE
|
|
490
|
+
*
|
|
491
|
+
* @param {Date} aStart Začátek prvního intervalu.
|
|
492
|
+
* @param {Date} aEnd Konec prvního intervalu.
|
|
493
|
+
* @param {Date} bStart Začátek druhého intervalu.
|
|
494
|
+
* @param {Date} bEnd Konec druhého intervalu.
|
|
495
|
+
* @returns {boolean} True pokud se intervaly překrývají.
|
|
496
|
+
*
|
|
497
|
+
* @example
|
|
498
|
+
* rangesIntersect(
|
|
499
|
+
* new Date(2025, 7, 25),
|
|
500
|
+
* new Date(2025, 7, 27),
|
|
501
|
+
* new Date(2025, 7, 27),
|
|
502
|
+
* new Date(2025, 7, 30)
|
|
503
|
+
* ) // → true
|
|
504
|
+
*/
|
|
505
|
+
export function rangesIntersect(aStart, aEnd, bStart, bEnd) {
|
|
506
|
+
return aStart <= bEnd && bStart <= aEnd;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Normalizuje event tak, aby měl:
|
|
511
|
+
* - převedené datumy na Date objekty bez časové složky
|
|
512
|
+
* - zajištěné pořadí (start <= end)
|
|
513
|
+
*
|
|
514
|
+
* Přidává do objektu:
|
|
515
|
+
* - `startDateObj` (Date)
|
|
516
|
+
* - `endDateObj` (Date)
|
|
517
|
+
*
|
|
518
|
+
* Pokud `endDate` není definováno, použije se `startDate`.
|
|
519
|
+
* Pokud jsou data obráceně (end < start), automaticky se prohodí.
|
|
520
|
+
*
|
|
521
|
+
* @param {{
|
|
522
|
+
* startDate: string | Date,
|
|
523
|
+
* endDate?: string | Date,
|
|
524
|
+
* [key: string]: any
|
|
525
|
+
* }} event Event s daty ve formátu "YYYY-MM-DD" nebo Date.
|
|
526
|
+
*
|
|
527
|
+
* @returns {{
|
|
528
|
+
* startDate: string | Date,
|
|
529
|
+
* endDate?: string | Date,
|
|
530
|
+
* startDateObj: Date,
|
|
531
|
+
* endDateObj: Date
|
|
532
|
+
* } & Record<string, any>} Normalizovaný event.
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* normalizeEvent({
|
|
536
|
+
* startDate: "2025-08-25",
|
|
537
|
+
* endDate: "2025-08-20"
|
|
538
|
+
* })
|
|
539
|
+
* // → startDateObj = 20.08.2025
|
|
540
|
+
* // → endDateObj = 25.08.2025
|
|
541
|
+
*
|
|
542
|
+
* @example
|
|
543
|
+
* normalizeEvent({
|
|
544
|
+
* startDate: "2025-08-25"
|
|
545
|
+
* })
|
|
546
|
+
* // → startDateObj = endDateObj = 25.08.2025
|
|
547
|
+
*/
|
|
548
|
+
export function normalizeEvent(event) {
|
|
549
|
+
const start = toLocalDate(event?.startDate || event?.startdate);
|
|
550
|
+
const end = toLocalDate(event?.endDate || event?.enddate || event?.startDate);
|
|
551
|
+
return start <= end
|
|
552
|
+
? { ...event, startDateObj: start, endDateObj: end, startDate: toIsoDate(start), endDate: toIsoDate(end) }
|
|
553
|
+
: { ...event, startDateObj: end, endDateObj: start, startDate: toIsoDate(end), endDate: toIsoDate(start) };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Rozbalí seznam událostí (events) na jemnější strukturu segmentů,
|
|
558
|
+
* kde každý segment reprezentuje polovinu týdne:
|
|
559
|
+
*
|
|
560
|
+
* - "L" (left) = pondělí–středa
|
|
561
|
+
* - "R" (right) = čtvrtek–neděle
|
|
562
|
+
*
|
|
563
|
+
* Nejprve vytvoří segmenty pro všechny týdny, poté do nich promítne události.
|
|
564
|
+
* Pokud událost zasahuje do segmentu (má průnik s jeho intervalem),
|
|
565
|
+
* nastaví se `toolAbbr` daného segmentu.
|
|
566
|
+
*
|
|
567
|
+
* Pokud více událostí zasahuje do stejného segmentu,
|
|
568
|
+
* poslední z nich v poli `events` má přednost (přepisuje předchozí).
|
|
569
|
+
*
|
|
570
|
+
* @param {Array<{
|
|
571
|
+
* id: string,
|
|
572
|
+
* firstHalfStart: Date,
|
|
573
|
+
* firstHalfEnd: Date,
|
|
574
|
+
* secondHalfStart: Date,
|
|
575
|
+
* secondHalfEnd: Date
|
|
576
|
+
* }>} weeks Seznam týdnů (výstup z buildAcademicWeeks).
|
|
577
|
+
*
|
|
578
|
+
* @param {Array<{
|
|
579
|
+
* startDate: string | Date,
|
|
580
|
+
* endDate?: string | Date,
|
|
581
|
+
* toolAbbr: string
|
|
582
|
+
* }>} events Seznam událostí (intervaly).
|
|
583
|
+
*
|
|
584
|
+
* @returns {Array<{
|
|
585
|
+
* key: string,
|
|
586
|
+
* weekId: string,
|
|
587
|
+
* side: "L" | "R",
|
|
588
|
+
* startDate: Date,
|
|
589
|
+
* endDate: Date,
|
|
590
|
+
* toolAbbr: string | null
|
|
591
|
+
* }>} Seznam segmentů (polovin týdnů) s přiřazenými nástroji.
|
|
592
|
+
*
|
|
593
|
+
* @example
|
|
594
|
+
* expandEventsToSegments(weeks, [
|
|
595
|
+
* { startDate: "2025-08-25", endDate: "2025-08-27", toolAbbr: "ZS" }
|
|
596
|
+
* ])
|
|
597
|
+
* // → segment L v daném týdnu bude mít toolAbbr "ZS"
|
|
598
|
+
*
|
|
599
|
+
* @example
|
|
600
|
+
* expandEventsToSegments(weeks, [
|
|
601
|
+
* { startDate: "2025-08-28", endDate: "2025-08-30", toolAbbr: "AJ" }
|
|
602
|
+
* ])
|
|
603
|
+
* // → segment R bude mít toolAbbr "AJ"
|
|
604
|
+
*/
|
|
605
|
+
export function expandEventsToSegments(weeks, events) {
|
|
606
|
+
const segments = [];
|
|
607
|
+
|
|
608
|
+
for (const week of weeks) {
|
|
609
|
+
segments.push({
|
|
610
|
+
key: `${week.id}-L`,
|
|
611
|
+
weekId: week.id,
|
|
612
|
+
side: "L",
|
|
613
|
+
startDate: week.firstHalfStart,
|
|
614
|
+
endDate: week.firstHalfEnd,
|
|
615
|
+
toolAbbr: null
|
|
616
|
+
});
|
|
617
|
+
segments.push({
|
|
618
|
+
key: `${week.id}-R`,
|
|
619
|
+
weekId: week.id,
|
|
620
|
+
side: "R",
|
|
621
|
+
startDate: week.secondHalfStart,
|
|
622
|
+
endDate: week.secondHalfEnd,
|
|
623
|
+
toolAbbr: null
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
for (const rawEvent of events || []) {
|
|
628
|
+
const event = normalizeEvent(rawEvent);
|
|
629
|
+
|
|
630
|
+
for (const segment of segments) {
|
|
631
|
+
if (
|
|
632
|
+
rangesIntersect(
|
|
633
|
+
event.startDateObj,
|
|
634
|
+
event.endDateObj,
|
|
635
|
+
segment.startDate,
|
|
636
|
+
segment.endDate
|
|
637
|
+
)
|
|
638
|
+
) {
|
|
639
|
+
segment.toolAbbr = event.toolAbbr;
|
|
640
|
+
segment.event = event
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
return segments;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Složí segmenty (poloviny týdnů) zpět do kompaktního seznamu událostí (events).
|
|
650
|
+
*
|
|
651
|
+
* Funguje jako inverzní operace k `expandEventsToSegments()`:
|
|
652
|
+
* - vezme jednotlivé segmenty (L/R půlky týdnů)
|
|
653
|
+
* - odstraní prázdné segmenty (`toolAbbr === null`)
|
|
654
|
+
* - spojí sousedící segmenty se stejným `toolAbbr` do jednoho časového intervalu
|
|
655
|
+
*
|
|
656
|
+
* Dva segmenty se sloučí pokud:
|
|
657
|
+
* - mají stejný `toolAbbr`
|
|
658
|
+
* - jejich intervaly na sebe přímo navazují (end + 1 den = start dalšího)
|
|
659
|
+
*
|
|
660
|
+
* Interně používá:
|
|
661
|
+
* - `_start` a `_end` (Date) pro výpočty
|
|
662
|
+
* - `startDate` a `endDate` (string ve formátu YYYY-MM-DD) pro výstup
|
|
663
|
+
*
|
|
664
|
+
* @param {Array<{
|
|
665
|
+
* key: string,
|
|
666
|
+
* weekId: string,
|
|
667
|
+
* side: "L" | "R",
|
|
668
|
+
* startDate: Date,
|
|
669
|
+
* endDate: Date,
|
|
670
|
+
* toolAbbr: string | null
|
|
671
|
+
* }>} segments Seznam segmentů (výstup z expandEventsToSegments).
|
|
672
|
+
*
|
|
673
|
+
* @returns {Array<{
|
|
674
|
+
* toolAbbr: string,
|
|
675
|
+
* startDate: string,
|
|
676
|
+
* endDate: string
|
|
677
|
+
* }>} Sloučený seznam událostí (intervaly).
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* // vstup (segmenty)
|
|
681
|
+
* [
|
|
682
|
+
* { toolAbbr: "ZS", startDate: 1.1, endDate: 3.1 },
|
|
683
|
+
* { toolAbbr: "ZS", startDate: 4.1, endDate: 7.1 }
|
|
684
|
+
* ]
|
|
685
|
+
*
|
|
686
|
+
* // výstup (events)
|
|
687
|
+
* [
|
|
688
|
+
* { toolAbbr: "ZS", startDate: "2025-01-01", endDate: "2025-01-07" }
|
|
689
|
+
* ]
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* // různé toolAbbr → nesloučí se
|
|
693
|
+
* [
|
|
694
|
+
* { toolAbbr: "ZS", ... },
|
|
695
|
+
* { toolAbbr: "AJ", ... }
|
|
696
|
+
* ]
|
|
697
|
+
*/
|
|
698
|
+
export function compressSegmentsToEvents(segments) {
|
|
699
|
+
const filtered = segments.filter(s => s.toolAbbr);
|
|
700
|
+
|
|
701
|
+
if (filtered.length === 0) return [];
|
|
702
|
+
|
|
703
|
+
const result = [];
|
|
704
|
+
let current = null;
|
|
705
|
+
|
|
706
|
+
for (const segment of filtered) {
|
|
707
|
+
if (!current) {
|
|
708
|
+
current = {
|
|
709
|
+
toolAbbr: segment.toolAbbr,
|
|
710
|
+
startDate: toIsoDate(segment.startDate),
|
|
711
|
+
endDate: toIsoDate(segment.endDate),
|
|
712
|
+
_start: segment.startDate,
|
|
713
|
+
_end: segment.endDate
|
|
714
|
+
};
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const isSameTool = current.toolAbbr === segment.toolAbbr;
|
|
719
|
+
const isContiguous = toIsoDate(addDays(current._end, 1)) === toIsoDate(segment.startDate);
|
|
720
|
+
|
|
721
|
+
if (isSameTool && isContiguous) {
|
|
722
|
+
current.endDate = toIsoDate(segment.endDate);
|
|
723
|
+
current._end = segment.endDate;
|
|
724
|
+
} else {
|
|
725
|
+
result.push({
|
|
726
|
+
toolAbbr: current.toolAbbr,
|
|
727
|
+
startDate: current.startDate,
|
|
728
|
+
endDate: current.endDate
|
|
729
|
+
});
|
|
730
|
+
current = {
|
|
731
|
+
toolAbbr: segment.toolAbbr,
|
|
732
|
+
startDate: toIsoDate(segment.startDate),
|
|
733
|
+
endDate: toIsoDate(segment.endDate),
|
|
734
|
+
_start: segment.startDate,
|
|
735
|
+
_end: segment.endDate
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (current) {
|
|
741
|
+
result.push({
|
|
742
|
+
toolAbbr: current.toolAbbr,
|
|
743
|
+
startDate: current.startDate,
|
|
744
|
+
endDate: current.endDate
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return result;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Aplikuje (nebo přepne) nástroj na konkrétní polovinu týdne
|
|
753
|
+
* a vrátí aktualizovaný seznam událostí (events).
|
|
754
|
+
*
|
|
755
|
+
* Postup:
|
|
756
|
+
* 1. Rozbalí existující events na segmenty (poloviny týdnů)
|
|
757
|
+
* 2. Najde cílový segment podle `weekId` a `side`
|
|
758
|
+
* 3. Přepne hodnotu:
|
|
759
|
+
* - pokud je již stejný nástroj → odstraní ho (toggle off)
|
|
760
|
+
* - jinak nastaví nový nástroj
|
|
761
|
+
* 4. Zkomprimuje segmenty zpět na intervaly (events)
|
|
762
|
+
*
|
|
763
|
+
* @param {Array<{
|
|
764
|
+
* id: string,
|
|
765
|
+
* firstHalfStart: Date,
|
|
766
|
+
* firstHalfEnd: Date,
|
|
767
|
+
* secondHalfStart: Date,
|
|
768
|
+
* secondHalfEnd: Date
|
|
769
|
+
* }>} weeks Seznam týdnů (výstup z buildAcademicWeeks).
|
|
770
|
+
*
|
|
771
|
+
* @param {Array<{
|
|
772
|
+
* startDate: string | Date,
|
|
773
|
+
* endDate?: string | Date,
|
|
774
|
+
* toolAbbr: string
|
|
775
|
+
* }>} events Aktuální seznam událostí.
|
|
776
|
+
*
|
|
777
|
+
* @param {string} weekId ID týdne (např. "2025-W35").
|
|
778
|
+
*
|
|
779
|
+
* @param {"L" | "R"} side Polovina týdne:
|
|
780
|
+
* - "L" = pondělí–středa
|
|
781
|
+
* - "R" = čtvrtek–neděle
|
|
782
|
+
*
|
|
783
|
+
* @param {string} selectedToolAbbr Zkratka vybraného nástroje (např. "ZS").
|
|
784
|
+
*
|
|
785
|
+
* @returns {Array<{
|
|
786
|
+
* toolAbbr: string,
|
|
787
|
+
* startDate: string,
|
|
788
|
+
* endDate: string
|
|
789
|
+
* }>} Nový seznam událostí po aplikaci změny.
|
|
790
|
+
*
|
|
791
|
+
* @example
|
|
792
|
+
* // nastaví ZS na levou polovinu týdne
|
|
793
|
+
* applyToolToHalf(weeks, events, "2025-W35", "L", "ZS")
|
|
794
|
+
*
|
|
795
|
+
* @example
|
|
796
|
+
* // kliknutí znovu stejným nástrojem → odstraní hodnotu
|
|
797
|
+
* applyToolToHalf(weeks, events, "2025-W35", "L", "ZS")
|
|
798
|
+
*/
|
|
799
|
+
export function applyToolToHalf(weeks, events, weekId, side, selectedToolAbbr) {
|
|
800
|
+
const segments = expandEventsToSegments(weeks, events);
|
|
801
|
+
const key = `${weekId}-${side}`;
|
|
802
|
+
const target = segments.find(s => s.key === key);
|
|
803
|
+
|
|
804
|
+
if (!target) return events;
|
|
805
|
+
|
|
806
|
+
target.toolAbbr = target.toolAbbr === selectedToolAbbr ? null : selectedToolAbbr;
|
|
807
|
+
|
|
808
|
+
return compressSegmentsToEvents(segments);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Aktualizuje události (events) jedné entity na základě změny
|
|
813
|
+
* v konkrétní polovině týdne.
|
|
814
|
+
*
|
|
815
|
+
* Interně:
|
|
816
|
+
* - použije `applyToolToHalf()` pro výpočet nového seznamu events
|
|
817
|
+
* - vrátí novou entitu s aktualizovanými events (immutabilní update)
|
|
818
|
+
*
|
|
819
|
+
* @param {{
|
|
820
|
+
* entity: {
|
|
821
|
+
* id: string,
|
|
822
|
+
* events?: Array<{
|
|
823
|
+
* startDate: string | Date,
|
|
824
|
+
* endDate?: string | Date,
|
|
825
|
+
* toolAbbr: string
|
|
826
|
+
* }>
|
|
827
|
+
* },
|
|
828
|
+
* weeks: Array<any>,
|
|
829
|
+
* weekId: string,
|
|
830
|
+
* side: "L" | "R",
|
|
831
|
+
* selectedToolAbbr: string
|
|
832
|
+
* }} params Parametry aktualizace.
|
|
833
|
+
*
|
|
834
|
+
* @returns {{
|
|
835
|
+
* id: string,
|
|
836
|
+
* events: Array<{
|
|
837
|
+
* toolAbbr: string,
|
|
838
|
+
* startDate: string,
|
|
839
|
+
* endDate: string
|
|
840
|
+
* }>
|
|
841
|
+
* } & Record<string, any>} Nová entita s upravenými events.
|
|
842
|
+
*
|
|
843
|
+
* @example
|
|
844
|
+
* updateEntityEventsForHalf({
|
|
845
|
+
* entity,
|
|
846
|
+
* weeks,
|
|
847
|
+
* weekId: "2025-W35",
|
|
848
|
+
* side: "L",
|
|
849
|
+
* selectedToolAbbr: "ZS"
|
|
850
|
+
* })
|
|
851
|
+
*/
|
|
852
|
+
export function updateEntityEventsForHalf({
|
|
853
|
+
entity,
|
|
854
|
+
weeks,
|
|
855
|
+
weekId,
|
|
856
|
+
side,
|
|
857
|
+
selectedToolAbbr
|
|
858
|
+
}) {
|
|
859
|
+
const updatedEvents = applyToolToHalf(
|
|
860
|
+
weeks,
|
|
861
|
+
entity.events || [],
|
|
862
|
+
weekId,
|
|
863
|
+
side,
|
|
864
|
+
selectedToolAbbr
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
return {
|
|
868
|
+
...entity,
|
|
869
|
+
events: updatedEvents
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Namapuje události (events) na jednotlivé týdny
|
|
875
|
+
* a přiřadí nástroje pro levou a pravou polovinu týdne.
|
|
876
|
+
*
|
|
877
|
+
* Výsledkem je seznam týdnů obohacený o:
|
|
878
|
+
* - `leftTool` (pondělí–středa)
|
|
879
|
+
* - `rightTool` (čtvrtek–neděle)
|
|
880
|
+
*
|
|
881
|
+
* Interně:
|
|
882
|
+
* - rozbalí events na segmenty (`expandEventsToSegments`)
|
|
883
|
+
* - vytvoří mapu segmentů podle `key`
|
|
884
|
+
* - pro každý týden dohledá odpovídající segmenty
|
|
885
|
+
*
|
|
886
|
+
* @param {Array<{
|
|
887
|
+
* id: string
|
|
888
|
+
* }>} weeks Seznam týdnů (z buildAcademicWeeks).
|
|
889
|
+
*
|
|
890
|
+
* @param {Array<{
|
|
891
|
+
* startDate: string | Date,
|
|
892
|
+
* endDate?: string | Date,
|
|
893
|
+
* toolAbbr: string
|
|
894
|
+
* }>} events Seznam událostí.
|
|
895
|
+
*
|
|
896
|
+
* @param {{ [abbreviation: string]: {
|
|
897
|
+
* id: string,
|
|
898
|
+
* name: string,
|
|
899
|
+
* abbreviation: string,
|
|
900
|
+
* color: string
|
|
901
|
+
* } }} toolMap Mapa nástrojů podle zkratky.
|
|
902
|
+
*
|
|
903
|
+
* @returns {Array<{
|
|
904
|
+
* id: string,
|
|
905
|
+
* leftTool: object | null,
|
|
906
|
+
* rightTool: object | null
|
|
907
|
+
* }>} Seznam týdnů s přiřazenými nástroji.
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* mapEventsToWeeks(weeks, events, toolMap)
|
|
911
|
+
* // → week.leftTool / week.rightTool obsahují tool objekty
|
|
912
|
+
*/
|
|
913
|
+
export function mapEventsToWeeks(weeks, events, toolMap) {
|
|
914
|
+
const segments = expandEventsToSegments(weeks, events);
|
|
915
|
+
const segmentMap = Object.fromEntries(segments.map(s => [s.key, s]));
|
|
916
|
+
|
|
917
|
+
return weeks.map(week => {
|
|
918
|
+
const leftAbbr = segmentMap[`${week.id}-L`]?.toolAbbr || null;
|
|
919
|
+
const rightAbbr = segmentMap[`${week.id}-R`]?.toolAbbr || null;
|
|
920
|
+
|
|
921
|
+
return {
|
|
922
|
+
...week,
|
|
923
|
+
leftTool: leftAbbr ? toolMap[leftAbbr] : null,
|
|
924
|
+
rightTool: rightAbbr ? toolMap[rightAbbr] : null
|
|
925
|
+
};
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Vypočítá agregaci nástrojů (toolAbbr) pro entitu
|
|
932
|
+
* na základě rozdělení na poloviny týdnů.
|
|
933
|
+
*
|
|
934
|
+
* Každý segment (polovina týdne) přispívá:
|
|
935
|
+
* - 0.5 týdne
|
|
936
|
+
*
|
|
937
|
+
* Výsledkem je objekt:
|
|
938
|
+
* - klíč = toolAbbr
|
|
939
|
+
* - hodnota = počet týdnů (včetně půlek, např. 12.5)
|
|
940
|
+
*
|
|
941
|
+
* @param {Array<any>} weeks Seznam týdnů.
|
|
942
|
+
*
|
|
943
|
+
* @param {Array<{
|
|
944
|
+
* startDate: string | Date,
|
|
945
|
+
* endDate?: string | Date,
|
|
946
|
+
* toolAbbr: string
|
|
947
|
+
* }>} events Seznam událostí.
|
|
948
|
+
*
|
|
949
|
+
* @returns {{ [toolAbbr: string]: number }} Agregace nástrojů v týdnech.
|
|
950
|
+
*
|
|
951
|
+
* @example
|
|
952
|
+
* aggregateEntityTools(weeks, events)
|
|
953
|
+
* // → { ZS: 12.5, LS: 8, Z: 3 }
|
|
954
|
+
*/
|
|
955
|
+
export function aggregateEntityTools(weeks, events) {
|
|
956
|
+
const segments = expandEventsToSegments(weeks, events);
|
|
957
|
+
|
|
958
|
+
const counts = {};
|
|
959
|
+
|
|
960
|
+
for (const segment of segments) {
|
|
961
|
+
if (!segment.toolAbbr) continue;
|
|
962
|
+
|
|
963
|
+
if (!counts[segment.toolAbbr]) {
|
|
964
|
+
counts[segment.toolAbbr] = 0;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
counts[segment.toolAbbr] += 0.5;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
return counts;
|
|
971
|
+
}
|