Danielzapirtan commited on
Commit
ae4866a
·
1 Parent(s): 778a0c6
Files changed (3) hide show
  1. index.html +3 -1107
  2. script.js +727 -0
  3. style.css +375 -0
index.html CHANGED
@@ -8,6 +8,7 @@
8
  <!-- PWA manifest + theme -->
9
  <link rel="manifest" href="manifest.json">
10
  <meta name="theme-color" content="#4CAF50">
 
11
 
12
  <!-- iOS PWA Support -->
13
  <meta name="apple-mobile-web-app-capable" content="yes">
@@ -19,383 +20,6 @@
19
  <link rel="apple-touch-icon" sizes="192x192" href="icons/icon-192.png">
20
  <link rel="apple-touch-icon" sizes="512x512" href="icons/icon-512.png">
21
 
22
- <style>
23
- :root {
24
- --primary-color: #4CAF50;
25
- --secondary-color: #2196F3;
26
- --light-bg: #f5f5f5;
27
- --border-color: #ddd;
28
- --workday-color: #e6f7ff;
29
- --holiday-color: #ffcccc;
30
- --leave-color: #ccffcc;
31
- --day-off-color: #ffebcc;
32
- }
33
-
34
- body {
35
- font-family: Arial, sans-serif;
36
- padding: 10px;
37
- background-color: var(--light-bg);
38
- margin: 0;
39
- }
40
-
41
- .container {
42
- max-width: 1200px;
43
- margin: 0 auto;
44
- }
45
-
46
- header {
47
- text-align: center;
48
- margin-bottom: 20px;
49
- padding-bottom: 15px;
50
- border-bottom: 1px solid var(--border-color);
51
- }
52
-
53
- h1 {
54
- color: var(--primary-color);
55
- margin-bottom: 10px;
56
- }
57
-
58
- .app-container {
59
- display: flex;
60
- flex-direction: column;
61
- gap: 20px;
62
- }
63
-
64
- .section {
65
- background-color: white;
66
- border-radius: 8px;
67
- padding: 15px;
68
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
69
- }
70
-
71
- .section-title {
72
- font-size: 1.2rem;
73
- margin-bottom: 15px;
74
- color: var(--primary-color);
75
- border-bottom: 1px solid var(--border-color);
76
- padding-bottom: 8px;
77
- }
78
-
79
- /* Calendar Styles */
80
- table {
81
- border-collapse: collapse;
82
- width: 100%;
83
- }
84
-
85
- th, td {
86
- border: 1px solid var(--border-color);
87
- padding: 8px;
88
- width: 14%;
89
- text-align: center;
90
- cursor: pointer;
91
- }
92
-
93
- th {
94
- background-color: #f2f2f2;
95
- }
96
-
97
- .unclickable {
98
- pointer-events: none;
99
- cursor: not-allowed;
100
- opacity: 0.6;
101
- }
102
-
103
- .doy0, .doy1 {
104
- background-color: lightyellow;
105
- }
106
-
107
- .doy2 {
108
- background-color: #aaffaa;
109
- }
110
-
111
- .doy3 {
112
- background-color: #aaaaff;
113
- }
114
-
115
- .doy4 {
116
- background-color: var(--leave-color);
117
- }
118
-
119
- .controls {
120
- margin-bottom: 15px;
121
- display: flex;
122
- justify-content: space-between;
123
- align-items: center;
124
- flex-wrap: wrap;
125
- }
126
-
127
- .year-btn {
128
- padding: 5px 10px;
129
- margin: 0 5px;
130
- cursor: pointer;
131
- border: 1px solid var(--border-color);
132
- border-radius: 4px;
133
- background-color: white;
134
- }
135
-
136
- .year-btn.active {
137
- font-weight: bold;
138
- background-color: var(--primary-color);
139
- color: white;
140
- }
141
-
142
- .holiday-buttons {
143
- margin: 10px 0;
144
- text-align: center;
145
- display: flex;
146
- flex-wrap: wrap;
147
- justify-content: center;
148
- gap: 5px;
149
- }
150
-
151
- .holiday-btn {
152
- padding: 8px 15px;
153
- cursor: pointer;
154
- background-color: #f0f0f0;
155
- border: 1px solid var(--border-color);
156
- border-radius: 4px;
157
- font-size: 0.9rem;
158
- }
159
-
160
- .holiday-btn:hover {
161
- background-color: #e0e0e0;
162
- }
163
-
164
- .holiday-result {
165
- margin: 10px 0;
166
- padding: 10px;
167
- background-color: #f9f9f9;
168
- color: #5555aa;
169
- border: 1px solid var(--border-color);
170
- border-radius: 4px;
171
- text-align: center;
172
- min-height: 20px;
173
- }
174
-
175
- #selectedMonth {
176
- font-weight: bold;
177
- font-size: 24px;
178
- color: #55aaaa;
179
- margin: 15px 0;
180
- text-align: center;
181
- }
182
-
183
- .user-info {
184
- margin-bottom: 10px;
185
- padding: 10px;
186
- background-color: #f0f8ff;
187
- border: 1px solid var(--border-color);
188
- border-radius: 4px;
189
- text-align: center;
190
- font-size: 14px;
191
- color: #555;
192
- }
193
-
194
- .stats {
195
- display: flex;
196
- justify-content: space-between;
197
- margin-top: 20px;
198
- padding: 10px;
199
- background-color: #f9f9f9;
200
- border-radius: 4px;
201
- flex-wrap: wrap;
202
- }
203
-
204
- .stat-item {
205
- text-align: center;
206
- margin: 5px;
207
- flex: 1;
208
- min-width: 120px;
209
- }
210
-
211
- .stat-value {
212
- font-size: 1.5rem;
213
- font-weight: bold;
214
- color: var(--primary-color);
215
- }
216
-
217
- .shift-controls {
218
- display: flex;
219
- align-items: center;
220
- justify-content: center;
221
- gap: 10px;
222
- margin: 15px 0;
223
- }
224
-
225
- .legend {
226
- display: flex;
227
- flex-wrap: wrap;
228
- gap: 10px;
229
- margin-top: 15px;
230
- justify-content: center;
231
- }
232
-
233
- .legend-item {
234
- display: flex;
235
- align-items: center;
236
- gap: 5px;
237
- }
238
-
239
- .legend-color {
240
- width: 20px;
241
- height: 20px;
242
- border: 1px solid var(--border-color);
243
- }
244
-
245
- .combined-planner {
246
- display: flex;
247
- flex-direction: column;
248
- gap: 20px;
249
- }
250
-
251
- .combined-controls {
252
- display: flex;
253
- justify-content: space-between;
254
- align-items: center;
255
- margin-bottom: 15px;
256
- flex-wrap: wrap;
257
- gap: 10px;
258
- }
259
-
260
- .month-navigation {
261
- display: flex;
262
- align-items: center;
263
- gap: 10px;
264
- }
265
-
266
- .nav-btn {
267
- padding: 5px 10px;
268
- background-color: var(--secondary-color);
269
- color: white;
270
- border: none;
271
- border-radius: 4px;
272
- cursor: pointer;
273
- }
274
-
275
- .combined-stats {
276
- display: flex;
277
- justify-content: space-around;
278
- margin: 15px 0;
279
- flex-wrap: wrap;
280
- }
281
-
282
- .combined-calendar {
283
- display: grid;
284
- grid-template-columns: repeat(7, 1fr);
285
- gap: 5px;
286
- }
287
-
288
- .combined-day {
289
- border: 1px solid var(--border-color);
290
- padding: 10px;
291
- text-align: center;
292
- cursor: pointer;
293
- min-height: 60px;
294
- display: flex;
295
- flex-direction: column;
296
- align-items: center;
297
- justify-content: center;
298
- }
299
-
300
- .day-number {
301
- font-weight: bold;
302
- margin-bottom: 5px;
303
- }
304
-
305
- .day-shift {
306
- font-size: 0.8rem;
307
- color: #666;
308
- }
309
-
310
- .combined-day.workday {
311
- background-color: var(--workday-color);
312
- }
313
-
314
- .combined-day.day-off {
315
- background-color: var(--day-off-color);
316
- }
317
-
318
- .combined-day.holiday {
319
- background-color: var(--holiday-color);
320
- }
321
-
322
- .combined-day.leave {
323
- background-color: var(--leave-color);
324
- }
325
-
326
- /* Optimization Controls */
327
- .optimization-controls {
328
- margin: 15px 0;
329
- text-align: center;
330
- }
331
-
332
- .optimize-btn {
333
- padding: 10px 20px;
334
- background-color: var(--primary-color);
335
- color: white;
336
- border: none;
337
- border-radius: 4px;
338
- cursor: pointer;
339
- font-size: 1rem;
340
- margin-bottom: 10px;
341
- }
342
-
343
- .optimize-btn:hover {
344
- background-color: #45a049;
345
- }
346
-
347
- .optimization-result {
348
- padding: 10px;
349
- border-radius: 4px;
350
- text-align: center;
351
- font-weight: bold;
352
- min-height: 20px;
353
- }
354
-
355
- .optimization-result.calculating {
356
- background-color: #fff3cd;
357
- color: #856404;
358
- border: 1px solid #ffeaa7;
359
- }
360
-
361
- .optimization-result.success {
362
- background-color: #d4edda;
363
- color: #155724;
364
- border: 1px solid #c3e6cb;
365
- }
366
-
367
- .optimization-result.error {
368
- background-color: #f8d7da;
369
- color: #721c24;
370
- border: 1px solid #f5c6cb;
371
- }
372
-
373
- @media (max-width: 768px) {
374
- .controls, .combined-controls {
375
- flex-direction: column;
376
- align-items: stretch;
377
- }
378
-
379
- .year-btn {
380
- margin: 2px;
381
- padding: 3px 6px;
382
- }
383
-
384
- .holiday-btn {
385
- padding: 6px 10px;
386
- font-size: 0.8rem;
387
- }
388
-
389
- .combined-day {
390
- min-height: 50px;
391
- padding: 5px;
392
- }
393
-
394
- .stat-item {
395
- min-width: 100px;
396
- }
397
- }
398
- </style>
399
  </head>
400
 
401
  <body>
@@ -498,734 +122,6 @@
498
  </div>
499
  </div>
500
 
501
- <script>
502
- // Holiday service class
503
- class RomanianHolidays {
504
- constructor() {
505
- this.baseUrl = 'https://date.nager.at/api/v3';
506
- }
507
-
508
- // Get all holidays for a year
509
- async getHolidays(year = new Date().getFullYear()) {
510
- try {
511
- const response = await fetch(`${this.baseUrl}/PublicHolidays/${year}/RO`);
512
- if (!response.ok) throw new Error('Network response was not ok');
513
- return await response.json();
514
- } catch (error) {
515
- console.error('Error fetching holidays:', error);
516
- return null;
517
- }
518
- }
519
-
520
- // Check if a specific date is a holiday
521
- async isHoliday(date = new Date()) {
522
- const year = date.getFullYear();
523
- const dateString = date.toISOString().split('T')[0]; // YYYY-MM-DD
524
-
525
- const holidays = await this.getHolidays(year);
526
- if (!holidays) return false;
527
-
528
- return holidays.some(holiday => holiday.date === dateString);
529
- }
530
-
531
- // Get upcoming holidays
532
- async getUpcomingHolidays(count = 5) {
533
- const currentYear = new Date().getFullYear();
534
- const holidays = await this.getHolidays(currentYear);
535
-
536
- if (!holidays) return [];
537
-
538
- const today = new Date().toISOString().split('T')[0];
539
-
540
- return holidays
541
- .filter(holiday => holiday.date >= today)
542
- .slice(0, count);
543
- }
544
-
545
- // Get holidays for multiple years
546
- async getHolidaysRange(startYear, endYear) {
547
- const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i);
548
- const promises = years.map(year => this.getHolidays(year));
549
-
550
- try {
551
- const results = await Promise.all(promises);
552
- return results.flat();
553
- } catch (error) {
554
- console.error('Error fetching holiday range:', error);
555
- return null;
556
- }
557
- }
558
- }
559
-
560
- // Global variables
561
- const monthNamesRo = ["ianuarie","februarie","martie","aprilie","mai","iunie","iulie","august","septembrie","octombrie","noiembrie","decembrie"];
562
- let currentYear = new Date().getFullYear();
563
- let selectedYear = currentYear;
564
- let selectedMonth = new Date().getMonth();
565
- let leaveDays = [];
566
- const refYear = 2017;
567
- const minYear = refYear + 1;
568
- const maxYear = 2037;
569
-
570
- // Planner variables
571
- let plannerYear = currentYear;
572
- let plannerMonth = selectedMonth;
573
- let plannerLeaveDays = [];
574
- let plannerWorkedDays = 0;
575
- let plannerTotalHours = 0;
576
-
577
- // Initialize holiday service
578
- const holidayService = new RomanianHolidays();
579
-
580
- function handleUrlParams() {
581
- const urlParams = new URLSearchParams(window.location.search);
582
- if (urlParams.has('user')) localStorage.setItem('user', urlParams.get('user'));
583
- if (urlParams.has('tura')) localStorage.setItem('tura', urlParams.get('tura'));
584
- updateUserInfo();
585
- }
586
-
587
- function updateUserInfo() {
588
- const storedUser = localStorage.getItem('user');
589
- const storedTura = localStorage.getItem('tura');
590
- const userInfoElement = document.getElementById('userInfo');
591
- if (storedUser || storedTura) {
592
- let info = 'Configurare: ';
593
- if (storedUser) info += `Utilizator: ${storedUser} `;
594
- if (storedTura) info += `Tură: ${storedTura}`;
595
- userInfoElement.textContent = info;
596
- userInfoElement.style.display = 'block';
597
- } else {
598
- userInfoElement.textContent = 'Eroare!';
599
- userInfoElement.style.display = 'block';
600
- }
601
- }
602
-
603
- function getTuraFromUrl() {
604
- let tura = 2;
605
- const urlParams = new URLSearchParams(window.location.search);
606
- if (urlParams.has("tura")) {
607
- tura = parseInt(urlParams.get("tura"));
608
- } else if (urlParams.has("user")) {
609
- const users = ["ljc1q", "xxtoo", "fras0", "l3hb4"];
610
- const idx = users.indexOf(urlParams.get("user"));
611
- if (idx >= 0) tura = idx + 1;
612
- } else {
613
- const storedTura = localStorage.getItem('tura');
614
- const storedUser = localStorage.getItem('user');
615
- if (storedTura) {
616
- tura = parseInt(storedTura);
617
- } else if (storedUser) {
618
- const users = ["ljc1q", "xxtoo", "fras0", "l3hb4"];
619
- const idx = users.indexOf(storedUser);
620
- if (idx >= 0) tura = idx + 1;
621
- }
622
- }
623
- if (tura % 2 === 0) tura = 6 - tura;
624
- return tura;
625
- }
626
-
627
- let plannerShift = getTuraFromUrl(); // Default shift
628
- document.getElementById("shift-planner").textContent = plannerShift;
629
-
630
- function isPWA() {
631
- return window.navigator.standalone === true ||
632
- window.matchMedia('(display-mode: standalone)').matches ||
633
- window.matchMedia('(display-mode: fullscreen)').matches;
634
- }
635
-
636
- function createYearButtons() {
637
- const yearButtonsContainer = document.getElementById('yearButtons');
638
- yearButtonsContainer.innerHTML = '';
639
- for (let year = currentYear - 3; year <= currentYear + 2; year++) {
640
- const button = document.createElement('button');
641
- button.className = 'year-btn';
642
- if (year === selectedYear) button.classList.add('active');
643
- button.textContent = year;
644
- button.onclick = function () {
645
- selectedYear = year;
646
- plannerYear = year;
647
- saveToLocalStorage();
648
- updateYearButtons();
649
- document.getElementById("holidayResult").innerHTML = ``;
650
- updatePlanner();
651
- };
652
- yearButtonsContainer.appendChild(button);
653
- }
654
- }
655
-
656
- function updateYearButtons() {
657
- const buttons = document.querySelectorAll('.year-btn');
658
- buttons.forEach(button => {
659
- if (parseInt(button.textContent) === selectedYear)
660
- button.classList.add('active');
661
- else
662
- button.classList.remove('active');
663
- });
664
- }
665
-
666
- function populateMonthSelect() {
667
- const monthSelect = document.getElementById('monthSelect');
668
- monthSelect.innerHTML = '';
669
- for (let month = 0; month < 12; month++) {
670
- const option = document.createElement('option');
671
- option.value = month;
672
- option.textContent = monthNamesRo[month];
673
- if (month === selectedMonth) option.selected = true;
674
- monthSelect.appendChild(option);
675
- }
676
- monthSelect.addEventListener('change', function () {
677
- selectedMonth = parseInt(monthSelect.value);
678
- plannerMonth = selectedMonth;
679
- saveToLocalStorage();
680
- document.getElementById("holidayResult").innerHTML = ``;
681
- updatePlanner();
682
- });
683
- }
684
-
685
- function loadFromLocalStorage() {
686
- const storedYear = localStorage.getItem('selectedYear');
687
- const storedMonth = localStorage.getItem('selectedMonth');
688
- const storedLeaveDays = localStorage.getItem('leaveDays');
689
-
690
- if (storedYear) selectedYear = parseInt(storedYear);
691
- if (storedMonth) selectedMonth = parseInt(storedMonth);
692
- if (storedLeaveDays) leaveDays = JSON.parse(storedLeaveDays);
693
-
694
- // Load planner data
695
- const storedPlannerMonth = localStorage.getItem('plannerMonth');
696
- const storedPlannerShift = localStorage.getItem('plannerShift');
697
- const storedPlannerLeaveDays = localStorage.getItem('plannerLeaveDays');
698
-
699
- if (storedPlannerMonth) plannerMonth = parseInt(storedPlannerMonth);
700
- if (storedPlannerShift) plannerShift = parseInt(storedPlannerShift);
701
- if (storedPlannerLeaveDays) plannerLeaveDays = JSON.parse(storedPlannerLeaveDays);
702
- }
703
-
704
- function saveToLocalStorage() {
705
- localStorage.setItem('selectedYear', selectedYear);
706
- localStorage.setItem('selectedMonth', selectedMonth);
707
- localStorage.setItem('leaveDays', JSON.stringify(leaveDays));
708
-
709
- // Save planner data
710
- localStorage.setItem('plannerMonth', plannerMonth);
711
- localStorage.setItem('plannerShift', plannerShift);
712
- localStorage.setItem('plannerLeaveDays', JSON.stringify(plannerLeaveDays));
713
- }
714
-
715
- function initializeControls() {
716
- handleUrlParams();
717
- loadFromLocalStorage();
718
- createYearButtons();
719
- populateMonthSelect();
720
-
721
- // Initialize planner controls
722
- document.getElementById('prev-year').addEventListener('click', () => {
723
- plannerYear = plannerYear - 1;
724
- selectedYear = plannerYear;
725
- if (plannerYear < minYear)
726
- plannerYear = minYear;
727
- updateYearButtons();
728
- updatePlanner();
729
- });
730
-
731
- document.getElementById('next-year').addEventListener('click', () => {
732
- plannerYear = plannerYear + 1;
733
- selectedYear = plannerYear;
734
- if (plannerYear > maxYear)
735
- plannerYear = maxYear;
736
- updateYearButtons();
737
- updatePlanner();
738
- });
739
-
740
- // Initialize planner controls
741
- document.getElementById('prev-month').addEventListener('click', () => {
742
- plannerMonth = (plannerMonth - 1 + 12) % 12;
743
- selectedMonth = plannerMonth;
744
- populateMonthSelect();
745
- updatePlanner();
746
- });
747
-
748
- document.getElementById('next-month').addEventListener('click', () => {
749
- plannerMonth = (plannerMonth + 1) % 12;
750
- selectedMonth = plannerMonth;
751
- populateMonthSelect();
752
- updatePlanner();
753
- });
754
-
755
- // Initialize optimize button
756
- document.getElementById('optimize-leave').addEventListener('click', optimizeLeaveDays);
757
-
758
- if (!isPWA()) {
759
- const urlParams = new URLSearchParams(window.location.search);
760
- const storedUser = localStorage.getItem('user');
761
- const storedTura = localStorage.getItem('tura');
762
- let urlChanged = false;
763
- if (storedUser && !urlParams.has('user')) { urlParams.set('user', storedUser); urlChanged = true; }
764
- if (storedTura && !urlParams.has('tura')) { urlParams.set('tura', storedTura); urlChanged = true; }
765
- if (urlChanged) {
766
- const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
767
- window.history.replaceState({}, '', newUrl);
768
- }
769
- }
770
- }
771
-
772
- function getEasterDate(year) {
773
- const a = year % 4, b = year % 7, c = year % 19;
774
- const d = (19 * c + 15) % 30;
775
- const e = (2 * a + 4 * b - d + 34) % 7;
776
- const month = Math.floor((d + e + 114) / 31);
777
- const day = ((d + e + 114) % 31) + 1;
778
- let date = new Date(year, month - 1, day);
779
- date.setDate(date.getDate() + 13);
780
- return date;
781
- }
782
-
783
- function getDayName(date) {
784
- const days = ['duminică', 'luni', 'marți', 'miercuri', 'joi', 'vineri', 'sâmbătă'];
785
- return days[date.getDay()];
786
- }
787
-
788
- function findClosestWorkShift(holidayDate, tura) {
789
- const date0 = new Date(2024, 0, 1);
790
- let closest = null, minDistance = Infinity;
791
- for (let offset = -10; offset <= 10; offset++) {
792
- let d = new Date(holidayDate);
793
- d.setDate(d.getDate() + offset);
794
- const daysDiff = Math.ceil((d - date0) / 86400000);
795
- const shift = (daysDiff + tura) % 4;
796
- if (shift === 2 || shift === 3) {
797
- let dist = Math.abs(offset);
798
- if (dist < minDistance) {
799
- minDistance = dist;
800
- closest = {
801
- date: d,
802
- shift: shift === 2 ? 'de zi' : 'de noapte',
803
- dayName: getDayName(d)
804
- };
805
- }
806
- }
807
- }
808
- return closest;
809
- }
810
-
811
- function checkHoliday(type) {
812
- const currentDate = new Date();
813
- const currentYear = currentDate.getFullYear();
814
- const tura = getTuraFromUrl();
815
- let date, name = '';
816
-
817
- if (type === "lcrt") {
818
- date = new Date();
819
- } else if (type === "lless") {
820
- date = new Date(selectedYear, selectedMonth - 1, new Date().getDate());
821
- } else if (type === "lgrt") {
822
- date = new Date(selectedYear, selectedMonth + 1, new Date().getDate());
823
- if (date.getFullYear() > currentYear + 2) date = new Date(currentYear + 2, 11, new Date().getDate());
824
- } else if (type === "lurm") {
825
- date = new Date(currentYear, new Date().getMonth() + 1, 15);
826
- } else if (type === "curm") {
827
- date = new Date(selectedYear, selectedMonth, 15);
828
- if (leaveDays) {
829
- while (true) {
830
- date = new Date(date.getFullYear(), date.getMonth() + 1, 15);
831
- if (date.getFullYear() > currentYear + 2) {
832
- date = new Date(currentYear - 1, 0, 15);
833
- }
834
- if (date.getFullYear() === selectedYear && date.getMonth() === selectedMonth) {
835
- break;
836
- }
837
- const currentMonth = `${date.getFullYear()}-${date.getMonth()}-`;
838
- const currentmlc = leaveDays.filter(day => day.startsWith(currentMonth)).length;
839
- if (currentmlc) break;
840
- }
841
- }
842
- } else {
843
- for (let year = currentYear; year <= currentYear + 10; year++) {
844
- if (type === 'paste') {
845
- date = getEasterDate(year);
846
- name = 'Paște';
847
- } else if (type === 'craciun') {
848
- date = new Date(year, 11, 25);
849
- name = 'Crăciun';
850
- } else if (type === 'revelion') {
851
- date = new Date(year, 0, 1);
852
- name = 'Revelion';
853
- }
854
- if (date > currentDate) break;
855
- }
856
- }
857
-
858
- const closest = findClosestWorkShift(date, tura);
859
- if (closest) {
860
- const m = monthNamesRo[closest.date.getMonth()];
861
- const y = closest.date.getFullYear();
862
- const d = closest.date.getDate();
863
- const text = `De ${name} ${y} sunt ${closest.shift} ${closest.dayName}, ${d} ${m} ${y}`;
864
- if (name) {
865
- document.getElementById('holidayResult').textContent = text;
866
- } else {
867
- document.getElementById("holidayResult").innerHTML = ``;
868
- }
869
- selectedYear = y;
870
- plannerYear = y;
871
- selectedMonth = closest.date.getMonth();
872
- plannerMonth = selectedMonth;
873
- }
874
-
875
- saveToLocalStorage();
876
- updateYearButtons();
877
- populateMonthSelect();
878
- updatePlanner();
879
- }
880
-
881
- // Planner functionality
882
- async function updatePlanner() {
883
- const calendar = document.getElementById("planner-calendar");
884
- const monthDisplay = document.getElementById("current-month");
885
- const yearDisplay = document.getElementById("current-year");
886
- const shiftDisplay = document.getElementById("shift-planner");
887
- const workedDaysDisplay = document.getElementById("worked-days");
888
- const leaveDaysDisplay = document.getElementById("leave-days");
889
- const totalHoursDisplay = document.getElementById("total-hours");
890
-
891
- // Update display
892
- monthDisplay.textContent = `${monthNamesRo[plannerMonth]}`;
893
- yearDisplay.textContent = `${plannerYear}`;
894
- plannerShift = getTuraFromUrl();
895
- if (plannerShift % 2 === 0) {
896
- plannerShift = 6 - plannerShift;
897
- }
898
- shiftDisplay.textContent = plannerShift;
899
-
900
- // Get holidays for the current month
901
- const holidays = await getHolidaysForMonth(plannerYear, plannerMonth);
902
-
903
- // Calculate days in month
904
- const daysInMonth = new Date(plannerYear, plannerMonth + 1, 0).getDate();
905
- const firstDay = (new Date(plannerYear, plannerMonth, 1).getDay() + 6) % 7;
906
-
907
- let targetHours = 0;
908
- for (let day = 1; day <= daysInMonth; day++) {
909
- if (!holidays.includes(day) && (new Date(plannerYear, plannerMonth, day).getDay() % 6) != 0)
910
- targetHours += 8;
911
- }
912
- document.getElementById("target-hours").textContent = targetHours;
913
-
914
- // Reset stats
915
- plannerWorkedDays = 0;
916
- plannerTotalHours = 0;
917
- let currentLeaveDays = 0;
918
-
919
- // Clear calendar
920
- calendar.innerHTML = '';
921
-
922
- // Add empty cells for days before the first day of the month
923
- for (let i = 0; i < firstDay; i++) {
924
- const emptyDay = document.createElement("div");
925
- emptyDay.classList.add("combined-day");
926
- emptyDay.classList.add("unclickable");
927
- calendar.appendChild(emptyDay);
928
- }
929
-
930
- // Add days of the month
931
- for (let day = 1; day <= daysInMonth; day++) {
932
- const dayElement = document.createElement("div");
933
- dayElement.classList.add("combined-day");
934
-
935
- // Add day number
936
- const dayNumber = document.createElement("div");
937
- dayNumber.classList.add("day-number");
938
- dayNumber.textContent = day;
939
- dayElement.appendChild(dayNumber);
940
-
941
- // Calculate shift
942
- const date0 = new Date(refYear, 0, 1);
943
- const currentDate = new Date(plannerYear, plannerMonth, day);
944
- const daysDiff = Math.ceil((currentDate - date0) / 86400000);
945
- let shift2 = plannerShift;
946
- if (shift2 % 2 === 0)
947
- shift2 = 6 - shift2;
948
- const shift = (daysDiff + shift2) % 4;
949
-
950
- // Add shift info
951
- const shiftInfo = document.createElement("div");
952
- shiftInfo.classList.add("day-shift");
953
-
954
- let shiftText = '';
955
- let isWorkDay = false;
956
-
957
- if (shift === 0 || shift === 1) {
958
- shiftText = 'Repaus';
959
- dayElement.classList.add("day-off");
960
- } else if (shift === 2) {
961
- shiftText = 'Zi';
962
- dayElement.classList.add("workday");
963
- isWorkDay = true;
964
- } else if (shift === 3) {
965
- shiftText = 'Noapte';
966
- dayElement.classList.add("workday");
967
- isWorkDay = true;
968
- }
969
-
970
- shiftInfo.textContent = shiftText;
971
- dayElement.appendChild(shiftInfo);
972
-
973
- // Check if holiday
974
- const isHoliday = holidays.includes(day) ||
975
- currentDate.getDay() === 0 || // Sunday
976
- currentDate.getDay() === 6; // Saturday
977
-
978
- if (isHoliday) {
979
- dayElement.classList.add("holiday");
980
- }
981
-
982
- // Check if leave day
983
- const dateKey = `${plannerYear}-${plannerMonth}-${day}`;
984
- const isLeaveDay = plannerLeaveDays.includes(dateKey);
985
-
986
- if (isLeaveDay) {
987
- dayElement.classList.add("leave");
988
- if (!isHoliday) {
989
- currentLeaveDays++;
990
- }
991
-
992
- // Calculate hours for leave day
993
- if (!isHoliday && !isWorkDay) {
994
- plannerTotalHours += 8; // 8 hours for leave on holiday/day off
995
- } else if (!isHoliday && isWorkDay) {
996
- plannerTotalHours += 8; // 8 hours for leave on work day
997
- }
998
- } else if (isWorkDay) {
999
- plannerWorkedDays++;
1000
- plannerTotalHours += 12; // 12 hours for work day
1001
- }
1002
-
1003
- // Add click event
1004
- dayElement.addEventListener('click', () => togglePlannerDay(plannerYear, plannerMonth, day));
1005
-
1006
- calendar.appendChild(dayElement);
1007
- }
1008
-
1009
- // Update stats
1010
- workedDaysDisplay.textContent = plannerWorkedDays;
1011
- leaveDaysDisplay.textContent = currentLeaveDays;
1012
- totalHoursDisplay.textContent = plannerTotalHours;
1013
-
1014
- // Update selected month display
1015
- const ldcount = plannerLeaveDays.length;
1016
- const currentMonth = `${plannerYear}-${plannerMonth}-`;
1017
- const currentmlc = plannerLeaveDays.filter(day => day.startsWith(currentMonth)).length;
1018
- const mlc2 = ldcount ? `(${currentmlc}/${ldcount})`: ``;
1019
- document.getElementById("selectedMonth").innerHTML = `Calendar ${monthNamesRo[plannerMonth]} ${plannerYear} ${mlc2}`;
1020
-
1021
- // Save to localStorage
1022
- saveToLocalStorage();
1023
- }
1024
-
1025
- async function getHolidaysForMonth(year, month) {
1026
- try {
1027
- const allHolidays = await holidayService.getHolidays(year);
1028
- if (!allHolidays) {
1029
- console.warn('Could not fetch holidays, using fallback');
1030
- // Return fallback holidays for each month
1031
- return getFallbackHolidays(year, month);
1032
- }
1033
-
1034
- // Filter holidays that are in the specified month and extract the day
1035
- const monthHolidays = allHolidays
1036
- .filter(holiday => {
1037
- const holidayMonth = parseInt(holiday.date.split('-')[1]);
1038
- return holidayMonth === month + 1; // API uses 1-based months
1039
- })
1040
- .map(holiday => parseInt(holiday.date.split('-')[2]));
1041
-
1042
- return monthHolidays;
1043
- } catch (error) {
1044
- console.error('Error getting holidays for month:', error);
1045
- return getFallbackHolidays(year, month);
1046
- }
1047
- }
1048
-
1049
- function getFallbackHolidays(year, month) {
1050
- // Fallback holidays for Romania
1051
- const holidays = {
1052
- 0: [1, 2, 24], // January: 1, 2 (Revelion), 24 (Unirea Principatelor Române)
1053
- 3: [], // April: Easter is dynamic, will be handled separately
1054
- 4: [1], // May: 1 (Ziua Muncii)
1055
- 5: [1], // June: 1 (Ziua Copilului)
1056
- 7: [15], // August: 15 (Adormirea Maicii Domnului)
1057
- 10: [1, 30], // November: 1 (Ziua Națională), 30 (Sf. Andrei)
1058
- 11: [1, 25, 26] // December: 1 (Ziua Națională), 25, 26 (Crăciun)
1059
- };
1060
-
1061
- // Add Easter for April if applicable
1062
- if (month === 3) {
1063
- const easter = getEasterDate(year);
1064
- if (easter.getMonth() === 3) {
1065
- holidays[3].push(easter.getDate());
1066
- holidays[3].push(easter.getDate() + 1); // Easter Monday
1067
- }
1068
- }
1069
-
1070
- // Add Easter for May if applicable (when Easter is in late April)
1071
- if (month === 4) {
1072
- const easter = getEasterDate(year);
1073
- if (easter.getMonth() === 3 && easter.getDate() > 25) {
1074
- holidays[4].push(easter.getDate() + 1); // Easter Monday
1075
- }
1076
- }
1077
-
1078
- return holidays[month] || [];
1079
- }
1080
-
1081
- function togglePlannerDay(year, month, day) {
1082
- const dateKey = `${year}-${month}-${day}`;
1083
- const index = plannerLeaveDays.indexOf(dateKey);
1084
-
1085
- if (index === -1) {
1086
- plannerLeaveDays.push(dateKey);
1087
- } else {
1088
- plannerLeaveDays.splice(index, 1);
1089
- }
1090
-
1091
- updatePlanner();
1092
- }
1093
-
1094
- // Schedule optimization algorithm
1095
- async function optimizeLeaveDays() {
1096
- const resultElement = document.getElementById('optimization-result');
1097
- resultElement.textContent = 'Se calculează...';
1098
- resultElement.className = 'optimization-result calculating';
1099
-
1100
- try {
1101
- const year = plannerYear;
1102
- const month = plannerMonth;
1103
-
1104
- // Get holidays for the current month
1105
- const holidays = await getHolidaysForMonth(year, month);
1106
-
1107
- // Add weekends to holidays
1108
- const daysInMonth = new Date(year, month + 1, 0).getDate();
1109
- for (let day = 1; day <= daysInMonth; day++) {
1110
- const date = new Date(year, month, day);
1111
- if (date.getDay() === 0 || date.getDay() === 6) { // Sunday or Saturday
1112
- if (!holidays.includes(day)) {
1113
- holidays.push(day);
1114
- }
1115
- }
1116
- }
1117
-
1118
- holidays.sort((a, b) => a - b);
1119
-
1120
- // Calculate target hours
1121
- let targetHours = 0;
1122
- for (let day = 1; day <= daysInMonth; day++) {
1123
- if (!holidays.includes(day) && (new Date(year, month, day).getDay() % 6) !== 0) {
1124
- targetHours += 8;
1125
- }
1126
- }
1127
-
1128
- const refDate = new Date(refYear, 0, 0);
1129
- let best = [-Infinity, -Infinity];
1130
- let bestChoice = [-1, -1];
1131
-
1132
- // Try all possible leave periods
1133
- for (let startDay = 1; startDay <= daysInMonth - 1; startDay++) {
1134
- for (let endDay = startDay + 1; endDay <= daysInMonth; endDay++) {
1135
- let score = [0, 0];
1136
- const leaves = [];
1137
-
1138
- // Create leave period
1139
- for (let day = startDay; day <= endDay; day++) {
1140
- leaves.push(day);
1141
- }
1142
-
1143
- score[1] = leaves.length;
1144
- let countHours = 0;
1145
-
1146
- // Calculate hours for this configuration
1147
- for (let day = 1; day <= daysInMonth; day++) {
1148
- const date1 = new Date(year, month, day);
1149
- const ecart = Math.round((date1 - refDate) / 86400000) + 7 - plannerShift;
1150
- // Check if it's a work day and not on leave
1151
- if (((ecart % 4) < 2) && !leaves.includes(day)) {
1152
- countHours += 12;
1153
- } else if (!holidays.includes(day) && leaves.includes(day)) {
1154
- // Leave day on regular day
1155
- countHours += 8;
1156
- }
1157
- // Holidays and weekends don't contribute to hours
1158
- }
1159
-
1160
- // Only consider configurations that match target hours
1161
- if (countHours === targetHours) {
1162
- // Calculate score: count work days that fall on holidays
1163
- for (let day = 1; day <= daysInMonth; day++) {
1164
- const date1 = new Date(year, month, day);
1165
- const ecart = Math.round((date1 - refDate) / 86400000) + 7 - plannerShift;
1166
-
1167
- // If it's a holiday, a work day, and not on leave
1168
- if (holidays.includes(day) && ((ecart % 4) < 2) && !leaves.includes(day)) {
1169
- score[0] += 1;
1170
- }
1171
- }
1172
- }
1173
-
1174
- // Update best choice
1175
- if (score[0] > best[0] || (score[0] === best[0] && score[1] > best[1])) {
1176
- best[0] = score[0];
1177
- best[1] = score[1];
1178
- bestChoice[0] = startDay;
1179
- bestChoice[1] = endDay;
1180
- }
1181
- }
1182
- }
1183
-
1184
- // Apply the best leave period
1185
- if (bestChoice[0] !== -1 && bestChoice[1] !== -1) {
1186
- // Clear existing leave days for this month
1187
- plannerLeaveDays = plannerLeaveDays.filter(day => {
1188
- const [y, m, d] = day.split('-').map(Number);
1189
- return !(y === year && m === month);
1190
- });
1191
-
1192
- // Add new leave days
1193
- for (let day = bestChoice[0]; day <= bestChoice[1]; day++) {
1194
- const dateKey = `${year}-${month}-${day}`;
1195
- plannerLeaveDays.push(dateKey);
1196
- }
1197
-
1198
- // Update the planner
1199
- updatePlanner();
1200
-
1201
- resultElement.textContent = `Concediu optimizat: zilele ${bestChoice[0]}-${bestChoice[1]} ${monthNamesRo[month]}`;
1202
- resultElement.className = 'optimization-result success';
1203
- } else {
1204
- resultElement.textContent = 'Nu s-a găsit o soluție optimă';
1205
- resultElement.className = 'optimization-result error';
1206
- }
1207
- } catch (error) {
1208
- console.error('Error optimizing leave days:', error);
1209
- resultElement.textContent = 'Eroare la optimizare';
1210
- resultElement.className = 'optimization-result error';
1211
- }
1212
- }
1213
-
1214
- // Initialize the app
1215
- window.addEventListener('load', function() {
1216
- initializeControls();
1217
- updatePlanner();
1218
- });
1219
-
1220
- document.addEventListener('visibilitychange', function() {
1221
- if (!document.hidden) updateUserInfo();
1222
- });
1223
-
1224
- if ("serviceWorker" in navigator) {
1225
- window.addEventListener('load', function() {
1226
- navigator.serviceWorker.register("sw.js");
1227
- });
1228
- }
1229
- </script>
1230
  </body>
1231
- </html>
 
8
  <!-- PWA manifest + theme -->
9
  <link rel="manifest" href="manifest.json">
10
  <meta name="theme-color" content="#4CAF50">
11
+ <link rel="stylesheet" href="style.css">
12
 
13
  <!-- iOS PWA Support -->
14
  <meta name="apple-mobile-web-app-capable" content="yes">
 
20
  <link rel="apple-touch-icon" sizes="192x192" href="icons/icon-192.png">
21
  <link rel="apple-touch-icon" sizes="512x512" href="icons/icon-512.png">
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  </head>
24
 
25
  <body>
 
122
  </div>
123
  </div>
124
 
125
+ <script src="script.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  </body>
127
+ </html>
script.js ADDED
@@ -0,0 +1,727 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Holiday service class
2
+ class RomanianHolidays {
3
+ constructor() {
4
+ this.baseUrl = 'https://date.nager.at/api/v3';
5
+ }
6
+
7
+ // Get all holidays for a year
8
+ async getHolidays(year = new Date().getFullYear()) {
9
+ try {
10
+ const response = await fetch(`${this.baseUrl}/PublicHolidays/${year}/RO`);
11
+ if (!response.ok) throw new Error('Network response was not ok');
12
+ return await response.json();
13
+ } catch (error) {
14
+ console.error('Error fetching holidays:', error);
15
+ return null;
16
+ }
17
+ }
18
+
19
+ // Check if a specific date is a holiday
20
+ async isHoliday(date = new Date()) {
21
+ const year = date.getFullYear();
22
+ const dateString = date.toISOString().split('T')[0]; // YYYY-MM-DD
23
+
24
+ const holidays = await this.getHolidays(year);
25
+ if (!holidays) return false;
26
+
27
+ return holidays.some(holiday => holiday.date === dateString);
28
+ }
29
+
30
+ // Get upcoming holidays
31
+ async getUpcomingHolidays(count = 5) {
32
+ const currentYear = new Date().getFullYear();
33
+ const holidays = await this.getHolidays(currentYear);
34
+
35
+ if (!holidays) return [];
36
+
37
+ const today = new Date().toISOString().split('T')[0];
38
+
39
+ return holidays
40
+ .filter(holiday => holiday.date >= today)
41
+ .slice(0, count);
42
+ }
43
+
44
+ // Get holidays for multiple years
45
+ async getHolidaysRange(startYear, endYear) {
46
+ const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i);
47
+ const promises = years.map(year => this.getHolidays(year));
48
+
49
+ try {
50
+ const results = await Promise.all(promises);
51
+ return results.flat();
52
+ } catch (error) {
53
+ console.error('Error fetching holiday range:', error);
54
+ return null;
55
+ }
56
+ }
57
+ }
58
+
59
+ // Global variables
60
+ const monthNamesRo = ["ianuarie","februarie","martie","aprilie","mai","iunie","iulie","august","septembrie","octombrie","noiembrie","decembrie"];
61
+ let currentYear = new Date().getFullYear();
62
+ let selectedYear = currentYear;
63
+ let selectedMonth = new Date().getMonth();
64
+ let leaveDays = [];
65
+ const refYear = 2017;
66
+ const minYear = refYear + 1;
67
+ const maxYear = 2037;
68
+
69
+ // Planner variables
70
+ let plannerYear = currentYear;
71
+ let plannerMonth = selectedMonth;
72
+ let plannerLeaveDays = [];
73
+ let plannerWorkedDays = 0;
74
+ let plannerTotalHours = 0;
75
+
76
+ // Initialize holiday service
77
+ const holidayService = new RomanianHolidays();
78
+
79
+ function handleUrlParams() {
80
+ const urlParams = new URLSearchParams(window.location.search);
81
+ if (urlParams.has('user')) localStorage.setItem('user', urlParams.get('user'));
82
+ if (urlParams.has('tura')) localStorage.setItem('tura', urlParams.get('tura'));
83
+ updateUserInfo();
84
+ }
85
+
86
+ function updateUserInfo() {
87
+ const storedUser = localStorage.getItem('user');
88
+ const storedTura = localStorage.getItem('tura');
89
+ const userInfoElement = document.getElementById('userInfo');
90
+ if (storedUser || storedTura) {
91
+ let info = 'Configurare: ';
92
+ if (storedUser) info += `Utilizator: ${storedUser} `;
93
+ if (storedTura) info += `Tură: ${storedTura}`;
94
+ userInfoElement.textContent = info;
95
+ userInfoElement.style.display = 'block';
96
+ } else {
97
+ userInfoElement.textContent = 'Eroare!';
98
+ userInfoElement.style.display = 'block';
99
+ }
100
+ }
101
+
102
+ function getTuraFromUrl() {
103
+ let tura = 2;
104
+ const urlParams = new URLSearchParams(window.location.search);
105
+ if (urlParams.has("tura")) {
106
+ tura = parseInt(urlParams.get("tura"));
107
+ } else if (urlParams.has("user")) {
108
+ const users = ["ljc1q", "xxtoo", "fras0", "l3hb4"];
109
+ const idx = users.indexOf(urlParams.get("user"));
110
+ if (idx >= 0) tura = idx + 1;
111
+ } else {
112
+ const storedTura = localStorage.getItem('tura');
113
+ const storedUser = localStorage.getItem('user');
114
+ if (storedTura) {
115
+ tura = parseInt(storedTura);
116
+ } else if (storedUser) {
117
+ const users = ["ljc1q", "xxtoo", "fras0", "l3hb4"];
118
+ const idx = users.indexOf(storedUser);
119
+ if (idx >= 0) tura = idx + 1;
120
+ }
121
+ }
122
+ if (tura % 2 === 0) tura = 6 - tura;
123
+ return tura;
124
+ }
125
+
126
+ let plannerShift = getTuraFromUrl(); // Default shift
127
+ document.getElementById("shift-planner").textContent = plannerShift;
128
+
129
+ function isPWA() {
130
+ return window.navigator.standalone === true ||
131
+ window.matchMedia('(display-mode: standalone)').matches ||
132
+ window.matchMedia('(display-mode: fullscreen)').matches;
133
+ }
134
+
135
+ function createYearButtons() {
136
+ const yearButtonsContainer = document.getElementById('yearButtons');
137
+ yearButtonsContainer.innerHTML = '';
138
+ for (let year = currentYear - 3; year <= currentYear + 2; year++) {
139
+ const button = document.createElement('button');
140
+ button.className = 'year-btn';
141
+ if (year === selectedYear) button.classList.add('active');
142
+ button.textContent = year;
143
+ button.onclick = function () {
144
+ selectedYear = year;
145
+ plannerYear = year;
146
+ saveToLocalStorage();
147
+ updateYearButtons();
148
+ document.getElementById("holidayResult").innerHTML = ``;
149
+ updatePlanner();
150
+ };
151
+ yearButtonsContainer.appendChild(button);
152
+ }
153
+ }
154
+
155
+ function updateYearButtons() {
156
+ const buttons = document.querySelectorAll('.year-btn');
157
+ buttons.forEach(button => {
158
+ if (parseInt(button.textContent) === selectedYear)
159
+ button.classList.add('active');
160
+ else
161
+ button.classList.remove('active');
162
+ });
163
+ }
164
+
165
+ function populateMonthSelect() {
166
+ const monthSelect = document.getElementById('monthSelect');
167
+ monthSelect.innerHTML = '';
168
+ for (let month = 0; month < 12; month++) {
169
+ const option = document.createElement('option');
170
+ option.value = month;
171
+ option.textContent = monthNamesRo[month];
172
+ if (month === selectedMonth) option.selected = true;
173
+ monthSelect.appendChild(option);
174
+ }
175
+ monthSelect.addEventListener('change', function () {
176
+ selectedMonth = parseInt(monthSelect.value);
177
+ plannerMonth = selectedMonth;
178
+ saveToLocalStorage();
179
+ document.getElementById("holidayResult").innerHTML = ``;
180
+ updatePlanner();
181
+ });
182
+ }
183
+
184
+ function loadFromLocalStorage() {
185
+ const storedYear = localStorage.getItem('selectedYear');
186
+ const storedMonth = localStorage.getItem('selectedMonth');
187
+ const storedLeaveDays = localStorage.getItem('leaveDays');
188
+
189
+ if (storedYear) selectedYear = parseInt(storedYear);
190
+ if (storedMonth) selectedMonth = parseInt(storedMonth);
191
+ if (storedLeaveDays) leaveDays = JSON.parse(storedLeaveDays);
192
+
193
+ // Load planner data
194
+ const storedPlannerMonth = localStorage.getItem('plannerMonth');
195
+ const storedPlannerShift = localStorage.getItem('plannerShift');
196
+ const storedPlannerLeaveDays = localStorage.getItem('plannerLeaveDays');
197
+
198
+ if (storedPlannerMonth) plannerMonth = parseInt(storedPlannerMonth);
199
+ if (storedPlannerShift) plannerShift = parseInt(storedPlannerShift);
200
+ if (storedPlannerLeaveDays) plannerLeaveDays = JSON.parse(storedPlannerLeaveDays);
201
+ }
202
+
203
+ function saveToLocalStorage() {
204
+ localStorage.setItem('selectedYear', selectedYear);
205
+ localStorage.setItem('selectedMonth', selectedMonth);
206
+ localStorage.setItem('leaveDays', JSON.stringify(leaveDays));
207
+
208
+ // Save planner data
209
+ localStorage.setItem('plannerMonth', plannerMonth);
210
+ localStorage.setItem('plannerShift', plannerShift);
211
+ localStorage.setItem('plannerLeaveDays', JSON.stringify(plannerLeaveDays));
212
+ }
213
+
214
+ function initializeControls() {
215
+ handleUrlParams();
216
+ loadFromLocalStorage();
217
+ createYearButtons();
218
+ populateMonthSelect();
219
+
220
+ // Initialize planner controls
221
+ document.getElementById('prev-year').addEventListener('click', () => {
222
+ plannerYear = plannerYear - 1;
223
+ selectedYear = plannerYear;
224
+ if (plannerYear < minYear)
225
+ plannerYear = minYear;
226
+ updateYearButtons();
227
+ updatePlanner();
228
+ });
229
+
230
+ document.getElementById('next-year').addEventListener('click', () => {
231
+ plannerYear = plannerYear + 1;
232
+ selectedYear = plannerYear;
233
+ if (plannerYear > maxYear)
234
+ plannerYear = maxYear;
235
+ updateYearButtons();
236
+ updatePlanner();
237
+ });
238
+
239
+ // Initialize planner controls
240
+ document.getElementById('prev-month').addEventListener('click', () => {
241
+ plannerMonth = (plannerMonth - 1 + 12) % 12;
242
+ selectedMonth = plannerMonth;
243
+ populateMonthSelect();
244
+ updatePlanner();
245
+ });
246
+
247
+ document.getElementById('next-month').addEventListener('click', () => {
248
+ plannerMonth = (plannerMonth + 1) % 12;
249
+ selectedMonth = plannerMonth;
250
+ populateMonthSelect();
251
+ updatePlanner();
252
+ });
253
+
254
+ // Initialize optimize button
255
+ document.getElementById('optimize-leave').addEventListener('click', optimizeLeaveDays);
256
+
257
+ if (!isPWA()) {
258
+ const urlParams = new URLSearchParams(window.location.search);
259
+ const storedUser = localStorage.getItem('user');
260
+ const storedTura = localStorage.getItem('tura');
261
+ let urlChanged = false;
262
+ if (storedUser && !urlParams.has('user')) { urlParams.set('user', storedUser); urlChanged = true; }
263
+ if (storedTura && !urlParams.has('tura')) { urlParams.set('tura', storedTura); urlChanged = true; }
264
+ if (urlChanged) {
265
+ const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
266
+ window.history.replaceState({}, '', newUrl);
267
+ }
268
+ }
269
+ }
270
+
271
+ function getEasterDate(year) {
272
+ const a = year % 4, b = year % 7, c = year % 19;
273
+ const d = (19 * c + 15) % 30;
274
+ const e = (2 * a + 4 * b - d + 34) % 7;
275
+ const month = Math.floor((d + e + 114) / 31);
276
+ const day = ((d + e + 114) % 31) + 1;
277
+ let date = new Date(year, month - 1, day);
278
+ date.setDate(date.getDate() + 13);
279
+ return date;
280
+ }
281
+
282
+ function getDayName(date) {
283
+ const days = ['duminică', 'luni', 'marți', 'miercuri', 'joi', 'vineri', 'sâmbătă'];
284
+ return days[date.getDay()];
285
+ }
286
+
287
+ function findClosestWorkShift(holidayDate, tura) {
288
+ const date0 = new Date(2024, 0, 1);
289
+ let closest = null, minDistance = Infinity;
290
+ for (let offset = -10; offset <= 10; offset++) {
291
+ let d = new Date(holidayDate);
292
+ d.setDate(d.getDate() + offset);
293
+ const daysDiff = Math.ceil((d - date0) / 86400000);
294
+ const shift = (daysDiff + tura) % 4;
295
+ if (shift === 2 || shift === 3) {
296
+ let dist = Math.abs(offset);
297
+ if (dist < minDistance) {
298
+ minDistance = dist;
299
+ closest = {
300
+ date: d,
301
+ shift: shift === 2 ? 'de zi' : 'de noapte',
302
+ dayName: getDayName(d)
303
+ };
304
+ }
305
+ }
306
+ }
307
+ return closest;
308
+ }
309
+
310
+ function checkHoliday(type) {
311
+ const currentDate = new Date();
312
+ const currentYear = currentDate.getFullYear();
313
+ const tura = getTuraFromUrl();
314
+ let date, name = '';
315
+
316
+ if (type === "lcrt") {
317
+ date = new Date();
318
+ } else if (type === "lless") {
319
+ date = new Date(selectedYear, selectedMonth - 1, new Date().getDate());
320
+ } else if (type === "lgrt") {
321
+ date = new Date(selectedYear, selectedMonth + 1, new Date().getDate());
322
+ if (date.getFullYear() > currentYear + 2) date = new Date(currentYear + 2, 11, new Date().getDate());
323
+ } else if (type === "lurm") {
324
+ date = new Date(currentYear, new Date().getMonth() + 1, 15);
325
+ } else if (type === "curm") {
326
+ date = new Date(selectedYear, selectedMonth, 15);
327
+ if (leaveDays) {
328
+ while (true) {
329
+ date = new Date(date.getFullYear(), date.getMonth() + 1, 15);
330
+ if (date.getFullYear() > currentYear + 2) {
331
+ date = new Date(currentYear - 1, 0, 15);
332
+ }
333
+ if (date.getFullYear() === selectedYear && date.getMonth() === selectedMonth) {
334
+ break;
335
+ }
336
+ const currentMonth = `${date.getFullYear()}-${date.getMonth()}-`;
337
+ const currentmlc = leaveDays.filter(day => day.startsWith(currentMonth)).length;
338
+ if (currentmlc) break;
339
+ }
340
+ }
341
+ } else {
342
+ for (let year = currentYear; year <= currentYear + 10; year++) {
343
+ if (type === 'paste') {
344
+ date = getEasterDate(year);
345
+ name = 'Paște';
346
+ } else if (type === 'craciun') {
347
+ date = new Date(year, 11, 25);
348
+ name = 'Crăciun';
349
+ } else if (type === 'revelion') {
350
+ date = new Date(year, 0, 1);
351
+ name = 'Revelion';
352
+ }
353
+ if (date > currentDate) break;
354
+ }
355
+ }
356
+
357
+ const closest = findClosestWorkShift(date, tura);
358
+ if (closest) {
359
+ const m = monthNamesRo[closest.date.getMonth()];
360
+ const y = closest.date.getFullYear();
361
+ const d = closest.date.getDate();
362
+ const text = `De ${name} ${y} sunt ${closest.shift} ${closest.dayName}, ${d} ${m} ${y}`;
363
+ if (name) {
364
+ document.getElementById('holidayResult').textContent = text;
365
+ } else {
366
+ document.getElementById("holidayResult").innerHTML = ``;
367
+ }
368
+ selectedYear = y;
369
+ plannerYear = y;
370
+ selectedMonth = closest.date.getMonth();
371
+ plannerMonth = selectedMonth;
372
+ }
373
+
374
+ saveToLocalStorage();
375
+ updateYearButtons();
376
+ populateMonthSelect();
377
+ updatePlanner();
378
+ }
379
+
380
+ // Planner functionality
381
+ async function updatePlanner() {
382
+ const calendar = document.getElementById("planner-calendar");
383
+ const monthDisplay = document.getElementById("current-month");
384
+ const yearDisplay = document.getElementById("current-year");
385
+ const shiftDisplay = document.getElementById("shift-planner");
386
+ const workedDaysDisplay = document.getElementById("worked-days");
387
+ const leaveDaysDisplay = document.getElementById("leave-days");
388
+ const totalHoursDisplay = document.getElementById("total-hours");
389
+
390
+ // Update display
391
+ monthDisplay.textContent = `${monthNamesRo[plannerMonth]}`;
392
+ yearDisplay.textContent = `${plannerYear}`;
393
+ plannerShift = getTuraFromUrl();
394
+ if (plannerShift % 2 === 0) {
395
+ plannerShift = 6 - plannerShift;
396
+ }
397
+ shiftDisplay.textContent = plannerShift;
398
+
399
+ // Get holidays for the current month
400
+ const holidays = await getHolidaysForMonth(plannerYear, plannerMonth);
401
+
402
+ // Calculate days in month
403
+ const daysInMonth = new Date(plannerYear, plannerMonth + 1, 0).getDate();
404
+ const firstDay = (new Date(plannerYear, plannerMonth, 1).getDay() + 6) % 7;
405
+
406
+ let targetHours = 0;
407
+ for (let day = 1; day <= daysInMonth; day++) {
408
+ if (!holidays.includes(day) && (new Date(plannerYear, plannerMonth, day).getDay() % 6) != 0)
409
+ targetHours += 8;
410
+ }
411
+ document.getElementById("target-hours").textContent = targetHours;
412
+
413
+ // Reset stats
414
+ plannerWorkedDays = 0;
415
+ plannerTotalHours = 0;
416
+ let currentLeaveDays = 0;
417
+
418
+ // Clear calendar
419
+ calendar.innerHTML = '';
420
+
421
+ // Add empty cells for days before the first day of the month
422
+ for (let i = 0; i < firstDay; i++) {
423
+ const emptyDay = document.createElement("div");
424
+ emptyDay.classList.add("combined-day");
425
+ emptyDay.classList.add("unclickable");
426
+ calendar.appendChild(emptyDay);
427
+ }
428
+
429
+ // Add days of the month
430
+ for (let day = 1; day <= daysInMonth; day++) {
431
+ const dayElement = document.createElement("div");
432
+ dayElement.classList.add("combined-day");
433
+
434
+ // Add day number
435
+ const dayNumber = document.createElement("div");
436
+ dayNumber.classList.add("day-number");
437
+ dayNumber.textContent = day;
438
+ dayElement.appendChild(dayNumber);
439
+
440
+ // Calculate shift
441
+ const date0 = new Date(refYear, 0, 1);
442
+ const currentDate = new Date(plannerYear, plannerMonth, day);
443
+ const daysDiff = Math.ceil((currentDate - date0) / 86400000);
444
+ let shift2 = plannerShift;
445
+ if (shift2 % 2 === 0)
446
+ shift2 = 6 - shift2;
447
+ const shift = (daysDiff + shift2) % 4;
448
+
449
+ // Add shift info
450
+ const shiftInfo = document.createElement("div");
451
+ shiftInfo.classList.add("day-shift");
452
+
453
+ let shiftText = '';
454
+ let isWorkDay = false;
455
+
456
+ if (shift === 0 || shift === 1) {
457
+ shiftText = 'Repaus';
458
+ dayElement.classList.add("day-off");
459
+ } else if (shift === 2) {
460
+ shiftText = 'Zi';
461
+ dayElement.classList.add("workday");
462
+ isWorkDay = true;
463
+ } else if (shift === 3) {
464
+ shiftText = 'Noapte';
465
+ dayElement.classList.add("workday");
466
+ isWorkDay = true;
467
+ }
468
+
469
+ shiftInfo.textContent = shiftText;
470
+ dayElement.appendChild(shiftInfo);
471
+
472
+ // Check if holiday
473
+ const isHoliday = holidays.includes(day) ||
474
+ currentDate.getDay() === 0 || // Sunday
475
+ currentDate.getDay() === 6; // Saturday
476
+
477
+ if (isHoliday) {
478
+ dayElement.classList.add("holiday");
479
+ }
480
+
481
+ // Check if leave day
482
+ const dateKey = `${plannerYear}-${plannerMonth}-${day}`;
483
+ const isLeaveDay = plannerLeaveDays.includes(dateKey);
484
+
485
+ if (isLeaveDay) {
486
+ dayElement.classList.add("leave");
487
+ if (!isHoliday) {
488
+ currentLeaveDays++;
489
+ }
490
+
491
+ // Calculate hours for leave day
492
+ if (!isHoliday && !isWorkDay) {
493
+ plannerTotalHours += 8; // 8 hours for leave on holiday/day off
494
+ } else if (!isHoliday && isWorkDay) {
495
+ plannerTotalHours += 8; // 8 hours for leave on work day
496
+ }
497
+ } else if (isWorkDay) {
498
+ plannerWorkedDays++;
499
+ plannerTotalHours += 12; // 12 hours for work day
500
+ }
501
+
502
+ // Add click event
503
+ dayElement.addEventListener('click', () => togglePlannerDay(plannerYear, plannerMonth, day));
504
+
505
+ calendar.appendChild(dayElement);
506
+ }
507
+
508
+ // Update stats
509
+ workedDaysDisplay.textContent = plannerWorkedDays;
510
+ leaveDaysDisplay.textContent = currentLeaveDays;
511
+ totalHoursDisplay.textContent = plannerTotalHours;
512
+
513
+ // Update selected month display
514
+ const ldcount = plannerLeaveDays.length;
515
+ const currentMonth = `${plannerYear}-${plannerMonth}-`;
516
+ const currentmlc = plannerLeaveDays.filter(day => day.startsWith(currentMonth)).length;
517
+ const mlc2 = ldcount ? `(${currentmlc}/${ldcount})`: ``;
518
+ document.getElementById("selectedMonth").innerHTML = `Calendar ${monthNamesRo[plannerMonth]} ${plannerYear} ${mlc2}`;
519
+
520
+ // Save to localStorage
521
+ saveToLocalStorage();
522
+ }
523
+
524
+ async function getHolidaysForMonth(year, month) {
525
+ try {
526
+ const allHolidays = await holidayService.getHolidays(year);
527
+ if (!allHolidays) {
528
+ console.warn('Could not fetch holidays, using fallback');
529
+ // Return fallback holidays for each month
530
+ return getFallbackHolidays(year, month);
531
+ }
532
+
533
+ // Filter holidays that are in the specified month and extract the day
534
+ const monthHolidays = allHolidays
535
+ .filter(holiday => {
536
+ const holidayMonth = parseInt(holiday.date.split('-')[1]);
537
+ return holidayMonth === month + 1; // API uses 1-based months
538
+ })
539
+ .map(holiday => parseInt(holiday.date.split('-')[2]));
540
+
541
+ return monthHolidays;
542
+ } catch (error) {
543
+ console.error('Error getting holidays for month:', error);
544
+ return getFallbackHolidays(year, month);
545
+ }
546
+ }
547
+
548
+ function getFallbackHolidays(year, month) {
549
+ // Fallback holidays for Romania
550
+ const holidays = {
551
+ 0: [1, 2, 24], // January: 1, 2 (Revelion), 24 (Unirea Principatelor Române)
552
+ 3: [], // April: Easter is dynamic, will be handled separately
553
+ 4: [1], // May: 1 (Ziua Muncii)
554
+ 5: [1], // June: 1 (Ziua Copilului)
555
+ 7: [15], // August: 15 (Adormirea Maicii Domnului)
556
+ 10: [1, 30], // November: 1 (Ziua Națională), 30 (Sf. Andrei)
557
+ 11: [1, 25, 26] // December: 1 (Ziua Națională), 25, 26 (Crăciun)
558
+ };
559
+
560
+ // Add Easter for April if applicable
561
+ if (month === 3) {
562
+ const easter = getEasterDate(year);
563
+ if (easter.getMonth() === 3) {
564
+ holidays[3].push(easter.getDate());
565
+ holidays[3].push(easter.getDate() + 1); // Easter Monday
566
+ }
567
+ }
568
+
569
+ // Add Easter for May if applicable (when Easter is in late April)
570
+ if (month === 4) {
571
+ const easter = getEasterDate(year);
572
+ if (easter.getMonth() === 3 && easter.getDate() > 25) {
573
+ holidays[4].push(easter.getDate() + 1); // Easter Monday
574
+ }
575
+ }
576
+
577
+ return holidays[month] || [];
578
+ }
579
+
580
+ function togglePlannerDay(year, month, day) {
581
+ const dateKey = `${year}-${month}-${day}`;
582
+ const index = plannerLeaveDays.indexOf(dateKey);
583
+
584
+ if (index === -1) {
585
+ plannerLeaveDays.push(dateKey);
586
+ } else {
587
+ plannerLeaveDays.splice(index, 1);
588
+ }
589
+
590
+ updatePlanner();
591
+ }
592
+
593
+ // Schedule optimization algorithm
594
+ async function optimizeLeaveDays() {
595
+ const resultElement = document.getElementById('optimization-result');
596
+ resultElement.textContent = 'Se calculează...';
597
+ resultElement.className = 'optimization-result calculating';
598
+
599
+ try {
600
+ const year = plannerYear;
601
+ const month = plannerMonth;
602
+
603
+ // Get holidays for the current month
604
+ const holidays = await getHolidaysForMonth(year, month);
605
+
606
+ // Add weekends to holidays
607
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
608
+ for (let day = 1; day <= daysInMonth; day++) {
609
+ const date = new Date(year, month, day);
610
+ if (date.getDay() === 0 || date.getDay() === 6) { // Sunday or Saturday
611
+ if (!holidays.includes(day)) {
612
+ holidays.push(day);
613
+ }
614
+ }
615
+ }
616
+
617
+ holidays.sort((a, b) => a - b);
618
+
619
+ // Calculate target hours
620
+ let targetHours = 0;
621
+ for (let day = 1; day <= daysInMonth; day++) {
622
+ if (!holidays.includes(day) && (new Date(year, month, day).getDay() % 6) !== 0) {
623
+ targetHours += 8;
624
+ }
625
+ }
626
+
627
+ const refDate = new Date(refYear, 0, 0);
628
+ let best = [-Infinity, -Infinity];
629
+ let bestChoice = [-1, -1];
630
+
631
+ // Try all possible leave periods
632
+ for (let startDay = 1; startDay <= daysInMonth - 1; startDay++) {
633
+ for (let endDay = startDay + 1; endDay <= daysInMonth; endDay++) {
634
+ let score = [0, 0];
635
+ const leaves = [];
636
+
637
+ // Create leave period
638
+ for (let day = startDay; day <= endDay; day++) {
639
+ leaves.push(day);
640
+ }
641
+
642
+ score[1] = leaves.length;
643
+ let countHours = 0;
644
+
645
+ // Calculate hours for this configuration
646
+ for (let day = 1; day <= daysInMonth; day++) {
647
+ const date1 = new Date(year, month, day);
648
+ const ecart = Math.round((date1 - refDate) / 86400000) + 7 - plannerShift;
649
+ // Check if it's a work day and not on leave
650
+ if (((ecart % 4) < 2) && !leaves.includes(day)) {
651
+ countHours += 12;
652
+ } else if (!holidays.includes(day) && leaves.includes(day)) {
653
+ // Leave day on regular day
654
+ countHours += 8;
655
+ }
656
+ // Holidays and weekends don't contribute to hours
657
+ }
658
+
659
+ // Only consider configurations that match target hours
660
+ if (countHours === targetHours) {
661
+ // Calculate score: count work days that fall on holidays
662
+ for (let day = 1; day <= daysInMonth; day++) {
663
+ const date1 = new Date(year, month, day);
664
+ const ecart = Math.round((date1 - refDate) / 86400000) + 7 - plannerShift;
665
+
666
+ // If it's a holiday, a work day, and not on leave
667
+ if (holidays.includes(day) && ((ecart % 4) < 2) && !leaves.includes(day)) {
668
+ score[0] += 1;
669
+ }
670
+ }
671
+ }
672
+
673
+ // Update best choice
674
+ if (score[0] > best[0] || (score[0] === best[0] && score[1] > best[1])) {
675
+ best[0] = score[0];
676
+ best[1] = score[1];
677
+ bestChoice[0] = startDay;
678
+ bestChoice[1] = endDay;
679
+ }
680
+ }
681
+ }
682
+
683
+ // Apply the best leave period
684
+ if (bestChoice[0] !== -1 && bestChoice[1] !== -1) {
685
+ // Clear existing leave days for this month
686
+ plannerLeaveDays = plannerLeaveDays.filter(day => {
687
+ const [y, m, d] = day.split('-').map(Number);
688
+ return !(y === year && m === month);
689
+ });
690
+
691
+ // Add new leave days
692
+ for (let day = bestChoice[0]; day <= bestChoice[1]; day++) {
693
+ const dateKey = `${year}-${month}-${day}`;
694
+ plannerLeaveDays.push(dateKey);
695
+ }
696
+
697
+ // Update the planner
698
+ updatePlanner();
699
+
700
+ resultElement.textContent = `Concediu optimizat: zilele ${bestChoice[0]}-${bestChoice[1]} ${monthNamesRo[month]}`;
701
+ resultElement.className = 'optimization-result success';
702
+ } else {
703
+ resultElement.textContent = 'Nu s-a găsit o soluție optimă';
704
+ resultElement.className = 'optimization-result error';
705
+ }
706
+ } catch (error) {
707
+ console.error('Error optimizing leave days:', error);
708
+ resultElement.textContent = 'Eroare la optimizare';
709
+ resultElement.className = 'optimization-result error';
710
+ }
711
+ }
712
+
713
+ // Initialize the app
714
+ window.addEventListener('load', function() {
715
+ initializeControls();
716
+ updatePlanner();
717
+ });
718
+
719
+ document.addEventListener('visibilitychange', function() {
720
+ if (!document.hidden) updateUserInfo();
721
+ });
722
+
723
+ if ("serviceWorker" in navigator) {
724
+ window.addEventListener('load', function() {
725
+ navigator.serviceWorker.register("sw.js");
726
+ });
727
+ }
style.css ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary-color: #4CAF50;
3
+ --secondary-color: #2196F3;
4
+ --light-bg: #f5f5f5;
5
+ --border-color: #ddd;
6
+ --workday-color: #e6f7ff;
7
+ --holiday-color: #ffcccc;
8
+ --leave-color: #ccffcc;
9
+ --day-off-color: #ffebcc;
10
+ }
11
+
12
+ body {
13
+ font-family: Arial, sans-serif;
14
+ padding: 10px;
15
+ background-color: var(--light-bg);
16
+ margin: 0;
17
+ }
18
+
19
+ .container {
20
+ max-width: 1200px;
21
+ margin: 0 auto;
22
+ }
23
+
24
+ header {
25
+ text-align: center;
26
+ margin-bottom: 20px;
27
+ padding-bottom: 15px;
28
+ border-bottom: 1px solid var(--border-color);
29
+ }
30
+
31
+ h1 {
32
+ color: var(--primary-color);
33
+ margin-bottom: 10px;
34
+ }
35
+
36
+ .app-container {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 20px;
40
+ }
41
+
42
+ .section {
43
+ background-color: white;
44
+ border-radius: 8px;
45
+ padding: 15px;
46
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
47
+ }
48
+
49
+ .section-title {
50
+ font-size: 1.2rem;
51
+ margin-bottom: 15px;
52
+ color: var(--primary-color);
53
+ border-bottom: 1px solid var(--border-color);
54
+ padding-bottom: 8px;
55
+ }
56
+
57
+ /* Calendar Styles */
58
+ table {
59
+ border-collapse: collapse;
60
+ width: 100%;
61
+ }
62
+
63
+ th, td {
64
+ border: 1px solid var(--border-color);
65
+ padding: 8px;
66
+ width: 14%;
67
+ text-align: center;
68
+ cursor: pointer;
69
+ }
70
+
71
+ th {
72
+ background-color: #f2f2f2;
73
+ }
74
+
75
+ .unclickable {
76
+ pointer-events: none;
77
+ cursor: not-allowed;
78
+ opacity: 0.6;
79
+ }
80
+
81
+ .doy0, .doy1 {
82
+ background-color: lightyellow;
83
+ }
84
+
85
+ .doy2 {
86
+ background-color: #aaffaa;
87
+ }
88
+
89
+ .doy3 {
90
+ background-color: #aaaaff;
91
+ }
92
+
93
+ .doy4 {
94
+ background-color: var(--leave-color);
95
+ }
96
+
97
+ .controls {
98
+ margin-bottom: 15px;
99
+ display: flex;
100
+ justify-content: space-between;
101
+ align-items: center;
102
+ flex-wrap: wrap;
103
+ }
104
+
105
+ .year-btn {
106
+ padding: 5px 10px;
107
+ margin: 0 5px;
108
+ cursor: pointer;
109
+ border: 1px solid var(--border-color);
110
+ border-radius: 4px;
111
+ background-color: white;
112
+ }
113
+
114
+ .year-btn.active {
115
+ font-weight: bold;
116
+ background-color: var(--primary-color);
117
+ color: white;
118
+ }
119
+
120
+ .holiday-buttons {
121
+ margin: 10px 0;
122
+ text-align: center;
123
+ display: flex;
124
+ flex-wrap: wrap;
125
+ justify-content: center;
126
+ gap: 5px;
127
+ }
128
+
129
+ .holiday-btn {
130
+ padding: 8px 15px;
131
+ cursor: pointer;
132
+ background-color: #f0f0f0;
133
+ border: 1px solid var(--border-color);
134
+ border-radius: 4px;
135
+ font-size: 0.9rem;
136
+ }
137
+
138
+ .holiday-btn:hover {
139
+ background-color: #e0e0e0;
140
+ }
141
+
142
+ .holiday-result {
143
+ margin: 10px 0;
144
+ padding: 10px;
145
+ background-color: #f9f9f9;
146
+ color: #5555aa;
147
+ border: 1px solid var(--border-color);
148
+ border-radius: 4px;
149
+ text-align: center;
150
+ min-height: 20px;
151
+ }
152
+
153
+ #selectedMonth {
154
+ font-weight: bold;
155
+ font-size: 24px;
156
+ color: #55aaaa;
157
+ margin: 15px 0;
158
+ text-align: center;
159
+ }
160
+
161
+ .user-info {
162
+ margin-bottom: 10px;
163
+ padding: 10px;
164
+ background-color: #f0f8ff;
165
+ border: 1px solid var(--border-color);
166
+ border-radius: 4px;
167
+ text-align: center;
168
+ font-size: 14px;
169
+ color: #555;
170
+ }
171
+
172
+ .stats {
173
+ display: flex;
174
+ justify-content: space-between;
175
+ margin-top: 20px;
176
+ padding: 10px;
177
+ background-color: #f9f9f9;
178
+ border-radius: 4px;
179
+ flex-wrap: wrap;
180
+ }
181
+
182
+ .stat-item {
183
+ text-align: center;
184
+ margin: 5px;
185
+ flex: 1;
186
+ min-width: 120px;
187
+ }
188
+
189
+ .stat-value {
190
+ font-size: 1.5rem;
191
+ font-weight: bold;
192
+ color: var(--primary-color);
193
+ }
194
+
195
+ .shift-controls {
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ gap: 10px;
200
+ margin: 15px 0;
201
+ }
202
+
203
+ .legend {
204
+ display: flex;
205
+ flex-wrap: wrap;
206
+ gap: 10px;
207
+ margin-top: 15px;
208
+ justify-content: center;
209
+ }
210
+
211
+ .legend-item {
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 5px;
215
+ }
216
+
217
+ .legend-color {
218
+ width: 20px;
219
+ height: 20px;
220
+ border: 1px solid var(--border-color);
221
+ }
222
+
223
+ .combined-planner {
224
+ display: flex;
225
+ flex-direction: column;
226
+ gap: 20px;
227
+ }
228
+
229
+ .combined-controls {
230
+ display: flex;
231
+ justify-content: space-between;
232
+ align-items: center;
233
+ margin-bottom: 15px;
234
+ flex-wrap: wrap;
235
+ gap: 10px;
236
+ }
237
+
238
+ .month-navigation {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 10px;
242
+ }
243
+
244
+ .nav-btn {
245
+ padding: 5px 10px;
246
+ background-color: var(--secondary-color);
247
+ color: white;
248
+ border: none;
249
+ border-radius: 4px;
250
+ cursor: pointer;
251
+ }
252
+
253
+ .combined-stats {
254
+ display: flex;
255
+ justify-content: space-around;
256
+ margin: 15px 0;
257
+ flex-wrap: wrap;
258
+ }
259
+
260
+ .combined-calendar {
261
+ display: grid;
262
+ grid-template-columns: repeat(7, 1fr);
263
+ gap: 5px;
264
+ }
265
+
266
+ .combined-day {
267
+ border: 1px solid var(--border-color);
268
+ padding: 10px;
269
+ text-align: center;
270
+ cursor: pointer;
271
+ min-height: 60px;
272
+ display: flex;
273
+ flex-direction: column;
274
+ align-items: center;
275
+ justify-content: center;
276
+ }
277
+
278
+ .day-number {
279
+ font-weight: bold;
280
+ margin-bottom: 5px;
281
+ }
282
+
283
+ .day-shift {
284
+ font-size: 0.8rem;
285
+ color: #666;
286
+ }
287
+
288
+ .combined-day.workday {
289
+ background-color: var(--workday-color);
290
+ }
291
+
292
+ .combined-day.day-off {
293
+ background-color: var(--day-off-color);
294
+ }
295
+
296
+ .combined-day.holiday {
297
+ background-color: var(--holiday-color);
298
+ }
299
+
300
+ .combined-day.leave {
301
+ background-color: var(--leave-color);
302
+ }
303
+
304
+ /* Optimization Controls */
305
+ .optimization-controls {
306
+ margin: 15px 0;
307
+ text-align: center;
308
+ }
309
+
310
+ .optimize-btn {
311
+ padding: 10px 20px;
312
+ background-color: var(--primary-color);
313
+ color: white;
314
+ border: none;
315
+ border-radius: 4px;
316
+ cursor: pointer;
317
+ font-size: 1rem;
318
+ margin-bottom: 10px;
319
+ }
320
+
321
+ .optimize-btn:hover {
322
+ background-color: #45a049;
323
+ }
324
+
325
+ .optimization-result {
326
+ padding: 10px;
327
+ border-radius: 4px;
328
+ text-align: center;
329
+ font-weight: bold;
330
+ min-height: 20px;
331
+ }
332
+
333
+ .optimization-result.calculating {
334
+ background-color: #fff3cd;
335
+ color: #856404;
336
+ border: 1px solid #ffeaa7;
337
+ }
338
+
339
+ .optimization-result.success {
340
+ background-color: #d4edda;
341
+ color: #155724;
342
+ border: 1px solid #c3e6cb;
343
+ }
344
+
345
+ .optimization-result.error {
346
+ background-color: #f8d7da;
347
+ color: #721c24;
348
+ border: 1px solid #f5c6cb;
349
+ }
350
+
351
+ @media (max-width: 768px) {
352
+ .controls, .combined-controls {
353
+ flex-direction: column;
354
+ align-items: stretch;
355
+ }
356
+
357
+ .year-btn {
358
+ margin: 2px;
359
+ padding: 3px 6px;
360
+ }
361
+
362
+ .holiday-btn {
363
+ padding: 6px 10px;
364
+ font-size: 0.8rem;
365
+ }
366
+
367
+ .combined-day {
368
+ min-height: 50px;
369
+ padding: 5px;
370
+ }
371
+
372
+ .stat-item {
373
+ min-width: 100px;
374
+ }
375
+ }