@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.
Files changed (68) hide show
  1. package/dist/cjs/index.js +17 -17
  2. package/dist/es/index.js +38 -31
  3. package/dist/umd/index.js +31 -31
  4. package/package.json +1 -1
  5. package/src/Base/Components/Link.jsx +13 -8
  6. package/src/Base/Mutations/General.jsx +3 -1
  7. package/src/Base/Mutations/Update.jsx +5 -0
  8. package/src/EventGQLModel/Components/A4Plan/A4Plan.jsx +363 -0
  9. package/src/EventGQLModel/Components/A4Plan/index.js +1 -0
  10. package/src/EventGQLModel/Components/CardCapsule.jsx +43 -0
  11. package/src/EventGQLModel/Components/Children.jsx +31 -0
  12. package/src/EventGQLModel/Components/ConfirmEdit.jsx +61 -0
  13. package/src/EventGQLModel/Components/Filter.jsx +14 -0
  14. package/src/EventGQLModel/Components/LargeCard.jsx +54 -0
  15. package/src/EventGQLModel/Components/Link.jsx +55 -0
  16. package/src/EventGQLModel/Components/LiveEdit.jsx +111 -0
  17. package/src/EventGQLModel/Components/MediumCard.jsx +39 -0
  18. package/src/EventGQLModel/Components/MediumContent.jsx +96 -0
  19. package/src/EventGQLModel/Components/MediumEditableContent.jsx +35 -0
  20. package/src/EventGQLModel/Components/Plan/PlanRow.jsx +470 -0
  21. package/src/EventGQLModel/Components/Plan/_utils.js +971 -0
  22. package/src/EventGQLModel/Components/Plan/calendarReducer.js +535 -0
  23. package/src/EventGQLModel/Components/Plan/index.js +3 -0
  24. package/src/EventGQLModel/Components/Table.jsx +7 -0
  25. package/src/EventGQLModel/Components/index.js +15 -0
  26. package/src/EventGQLModel/Mutations/Create.jsx +202 -0
  27. package/src/EventGQLModel/Mutations/Delete.jsx +173 -0
  28. package/src/EventGQLModel/Mutations/InteractiveMutations.jsx +30 -0
  29. package/src/EventGQLModel/Mutations/Update.jsx +147 -0
  30. package/src/EventGQLModel/Mutations/helpers.jsx +7 -0
  31. package/src/EventGQLModel/Pages/PageBase.jsx +56 -0
  32. package/src/EventGQLModel/Pages/PageCreateItem.jsx +28 -0
  33. package/src/EventGQLModel/Pages/PageDeleteItem.jsx +16 -0
  34. package/src/EventGQLModel/Pages/PageNavbar.jsx +160 -0
  35. package/src/EventGQLModel/Pages/PagePlan.jsx +42 -0
  36. package/src/EventGQLModel/Pages/PageReadItem.jsx +11 -0
  37. package/src/EventGQLModel/Pages/PageReadItemEx.jsx +42 -0
  38. package/src/EventGQLModel/Pages/PageSubevents.jsx +43 -0
  39. package/src/EventGQLModel/Pages/PageUpdateItem.jsx +14 -0
  40. package/src/EventGQLModel/Pages/PageVector.jsx +80 -0
  41. package/src/EventGQLModel/Pages/RouterSegment.jsx +82 -0
  42. package/src/EventGQLModel/Pages/index.js +2 -0
  43. package/src/EventGQLModel/Queries/DeleteAsyncAction.jsx +32 -0
  44. package/src/EventGQLModel/Queries/Fragments.jsx +123 -0
  45. package/src/EventGQLModel/Queries/InsertAsyncAction.jsx +40 -0
  46. package/src/EventGQLModel/Queries/ReadAsyncAction.jsx +44 -0
  47. package/src/EventGQLModel/Queries/ReadPageAsyncAction.jsx +13 -0
  48. package/src/EventGQLModel/Queries/ReadSubEventsAsyncAction.jsx +44 -0
  49. package/src/EventGQLModel/Queries/SearchAsyncAction.jsx +16 -0
  50. package/src/EventGQLModel/Queries/UpdateAsyncAction.jsx +40 -0
  51. package/src/EventGQLModel/Queries/index.js +6 -0
  52. package/src/EventGQLModel/Scalars/ScalarAttribute.jsx +54 -0
  53. package/src/EventGQLModel/Scalars/TemplateScalarAttribute.jsx +88 -0
  54. package/src/EventGQLModel/Scalars/index.js +1 -0
  55. package/src/EventGQLModel/Vectors/TemplateVectorsAttribute.jsx +326 -0
  56. package/src/EventGQLModel/Vectors/VectorAttribute.jsx +56 -0
  57. package/src/EventGQLModel/Vectors/index.js +1 -0
  58. package/src/EventGQLModel/WhatToDo.md +44 -0
  59. package/src/EventGQLModel/index.js +71 -0
  60. package/src/GroupGQLModel/Mutations/Create.jsx +8 -2
  61. package/src/GroupGQLModel/Mutations/Delete.jsx +8 -2
  62. package/src/GroupGQLModel/Mutations/Update.jsx +8 -8
  63. package/src/GroupGQLModel/Queries/Fragments.jsx +17 -1
  64. package/src/GroupGQLModel/Scalars/RBACObject.jsx +17 -5
  65. package/src/GroupGQLModel/Vectors/GroupMemberships.jsx +1 -1
  66. package/src/UserGQLModel/Components/MediumContent.jsx +9 -3
  67. package/src/UserGQLModel/Queries/Fragments.jsx +6 -0
  68. 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
+ }