Spaces:
Running
Running
| <html lang="ro"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <title>Calendar și Planificator Concedii</title> | |
| <!-- PWA manifest + theme --> | |
| <link rel="manifest" href="manifest.json"> | |
| <meta name="theme-color" content="#4CAF50"> | |
| <!-- iOS PWA Support --> | |
| <meta name="apple-mobile-web-app-capable" content="yes"> | |
| <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> | |
| <meta name="apple-mobile-web-app-title" content="Calendar"> | |
| <link rel="icon" href="icons/icon-192.png"> | |
| <link rel="apple-touch-icon" href="icons/icon-192.png"> | |
| <link rel="apple-touch-icon" sizes="180x180" href="icons/icon-192.png"> | |
| <link rel="apple-touch-icon" sizes="192x192" href="icons/icon-192.png"> | |
| <link rel="apple-touch-icon" sizes="512x512" href="icons/icon-512.png"> | |
| <style> | |
| :root { | |
| --primary-color: #4CAF50; | |
| --secondary-color: #2196F3; | |
| --light-bg: #f5f5f5; | |
| --border-color: #ddd; | |
| --workday-color: #e6f7ff; | |
| --holiday-color: #ffcccc; | |
| --leave-color: #ccffcc; | |
| --day-off-color: #ffebcc; | |
| } | |
| body { | |
| font-family: Arial, sans-serif; | |
| padding: 10px; | |
| background-color: var(--light-bg); | |
| margin: 0; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| text-align: center; | |
| margin-bottom: 20px; | |
| padding-bottom: 15px; | |
| border-bottom: 1px solid var(--border-color); | |
| } | |
| h1 { | |
| color: var(--primary-color); | |
| margin-bottom: 10px; | |
| } | |
| .app-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .section { | |
| background-color: white; | |
| border-radius: 8px; | |
| padding: 15px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.1); | |
| } | |
| .section-title { | |
| font-size: 1.2rem; | |
| margin-bottom: 15px; | |
| color: var(--primary-color); | |
| border-bottom: 1px solid var(--border-color); | |
| padding-bottom: 8px; | |
| } | |
| /* Calendar Styles */ | |
| table { | |
| border-collapse: collapse; | |
| width: 100%; | |
| } | |
| th, td { | |
| border: 1px solid var(--border-color); | |
| padding: 8px; | |
| width: 14%; | |
| text-align: center; | |
| cursor: pointer; | |
| } | |
| th { | |
| background-color: #f2f2f2; | |
| } | |
| .unclickable { | |
| pointer-events: none; | |
| cursor: not-allowed; | |
| opacity: 0.6; | |
| } | |
| .doy0, .doy1 { | |
| background-color: lightyellow; | |
| } | |
| .doy2 { | |
| background-color: #aaffaa; | |
| } | |
| .doy3 { | |
| background-color: #aaaaff; | |
| } | |
| .doy4 { | |
| background-color: var(--leave-color); | |
| } | |
| .controls { | |
| margin-bottom: 15px; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .year-btn { | |
| padding: 5px 10px; | |
| margin: 0 5px; | |
| cursor: pointer; | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| background-color: white; | |
| } | |
| .year-btn.active { | |
| font-weight: bold; | |
| background-color: var(--primary-color); | |
| color: white; | |
| } | |
| .holiday-buttons { | |
| margin: 10px 0; | |
| text-align: center; | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 5px; | |
| } | |
| .holiday-btn { | |
| padding: 8px 15px; | |
| cursor: pointer; | |
| background-color: #f0f0f0; | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| font-size: 0.9rem; | |
| } | |
| .holiday-btn:hover { | |
| background-color: #e0e0e0; | |
| } | |
| .holiday-result { | |
| margin: 10px 0; | |
| padding: 10px; | |
| background-color: #f9f9f9; | |
| color: #5555aa; | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| text-align: center; | |
| min-height: 20px; | |
| } | |
| #selectedMonth { | |
| font-weight: bold; | |
| font-size: 24px; | |
| color: #55aaaa; | |
| margin: 15px 0; | |
| text-align: center; | |
| } | |
| .user-info { | |
| margin-bottom: 10px; | |
| padding: 10px; | |
| background-color: #f0f8ff; | |
| border: 1px solid var(--border-color); | |
| border-radius: 4px; | |
| text-align: center; | |
| font-size: 14px; | |
| color: #555; | |
| } | |
| .stats { | |
| display: flex; | |
| justify-content: space-between; | |
| margin-top: 20px; | |
| padding: 10px; | |
| background-color: #f9f9f9; | |
| border-radius: 4px; | |
| flex-wrap: wrap; | |
| } | |
| .stat-item { | |
| text-align: center; | |
| margin: 5px; | |
| flex: 1; | |
| min-width: 120px; | |
| } | |
| .stat-value { | |
| font-size: 1.5rem; | |
| font-weight: bold; | |
| color: var(--primary-color); | |
| } | |
| .shift-controls { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 10px; | |
| margin: 15px 0; | |
| } | |
| .legend { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| margin-top: 15px; | |
| justify-content: center; | |
| } | |
| .legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .legend-color { | |
| width: 20px; | |
| height: 20px; | |
| border: 1px solid var(--border-color); | |
| } | |
| .combined-planner { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .combined-controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 15px; | |
| flex-wrap: wrap; | |
| gap: 10px; | |
| } | |
| .month-navigation { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .nav-btn { | |
| padding: 5px 10px; | |
| background-color: var(--secondary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .combined-stats { | |
| display: flex; | |
| justify-content: space-around; | |
| margin: 15px 0; | |
| flex-wrap: wrap; | |
| } | |
| .combined-calendar { | |
| display: grid; | |
| grid-template-columns: repeat(7, 1fr); | |
| gap: 5px; | |
| } | |
| .combined-day { | |
| border: 1px solid var(--border-color); | |
| padding: 10px; | |
| text-align: center; | |
| cursor: pointer; | |
| min-height: 60px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .day-number { | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .day-shift { | |
| font-size: 0.8rem; | |
| color: #666; | |
| } | |
| .combined-day.workday { | |
| background-color: var(--workday-color); | |
| } | |
| .combined-day.day-off { | |
| background-color: var(--day-off-color); | |
| } | |
| .combined-day.holiday { | |
| background-color: var(--holiday-color); | |
| } | |
| .combined-day.leave { | |
| background-color: var(--leave-color); | |
| } | |
| /* Optimization Controls */ | |
| .optimization-controls { | |
| margin: 15px 0; | |
| text-align: center; | |
| } | |
| .optimize-btn { | |
| padding: 10px 20px; | |
| background-color: var(--primary-color); | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| margin-bottom: 10px; | |
| } | |
| .optimize-btn:hover { | |
| background-color: #45a049; | |
| } | |
| .optimization-result { | |
| padding: 10px; | |
| border-radius: 4px; | |
| text-align: center; | |
| font-weight: bold; | |
| min-height: 20px; | |
| } | |
| .optimization-result.calculating { | |
| background-color: #fff3cd; | |
| color: #856404; | |
| border: 1px solid #ffeaa7; | |
| } | |
| .optimization-result.success { | |
| background-color: #d4edda; | |
| color: #155724; | |
| border: 1px solid #c3e6cb; | |
| } | |
| .optimization-result.error { | |
| background-color: #f8d7da; | |
| color: #721c24; | |
| border: 1px solid #f5c6cb; | |
| } | |
| @media (max-width: 768px) { | |
| .controls, .combined-controls { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .year-btn { | |
| margin: 2px; | |
| padding: 3px 6px; | |
| } | |
| .holiday-btn { | |
| padding: 6px 10px; | |
| font-size: 0.8rem; | |
| } | |
| .combined-day { | |
| min-height: 50px; | |
| padding: 5px; | |
| } | |
| .stat-item { | |
| min-width: 100px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>Calendar și Planificator Concedii</h1> | |
| <div id="userInfo" class="user-info"></div> | |
| </header> | |
| <div class="app-container"> | |
| <!-- Combined Calendar and Planner Section --> | |
| <div class="section"> | |
| <div class="section-title">Calendar și Planificator Concedii</div> | |
| <div class="combined-planner"> | |
| <div class="combined-controls"> | |
| <div class="month-navigation"> | |
| <button class="nav-btn" id="prev-month"><</button> | |
| <span id="current-month">Ianuarie</span> | |
| <button class="nav-btn" id="next-month">></button> | |
| </div> | |
| <div class="year-navigation"> | |
| <button class="nav-btn" id="prev-year"><</button> | |
| <span id="current-year">2024</span> | |
| <button class="nav-btn" id="next-year">></button> | |
| </div> | |
| <div class="shift-controls"> | |
| <span>Tură: </span> | |
| <span id="shift-planner">3</span> | |
| </div> | |
| </div> | |
| <div class="controls"> | |
| <div id="yearButtons"></div> | |
| <select id="monthSelect"></select> | |
| </div> | |
| <div class="holiday-buttons"> | |
| <button class="holiday-btn" onclick="checkHoliday('lless')">L<</button> | |
| <button class="holiday-btn" onclick="checkHoliday('lcrt')">Acum</button> | |
| <button class="holiday-btn" onclick="checkHoliday('lurm')">Luna urm</button> | |
| <button class="holiday-btn" onclick="checkHoliday('lgrt')">L></button> | |
| <button class="holiday-btn" onclick="checkHoliday('curm')">C urm</button> | |
| <button class="holiday-btn" onclick="checkHoliday('paste')">Paște</button> | |
| <button class="holiday-btn" onclick="checkHoliday('craciun')">Crăciun</button> | |
| <button class="holiday-btn" onclick="checkHoliday('revelion')">Revelion</button> | |
| </div> | |
| <div id="holidayResult" class="holiday-result"></div> | |
| <div id="selectedMonth"></div> | |
| <div class="optimization-controls"> | |
| <button class="optimize-btn" id="optimize-leave">Optimizează Concediu</button> | |
| <div id="optimization-result" class="optimization-result"></div> | |
| </div> | |
| <div class="combined-stats"> | |
| <div class="stat-item"> | |
| <div>Zile Lucrate</div> | |
| <div class="stat-value" id="worked-days">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Zile Concediu</div> | |
| <div class="stat-value" id="leave-days">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Ore Total</div> | |
| <div class="stat-value" id="total-hours">0</div> | |
| </div> | |
| <div class="stat-item"> | |
| <div>Ore Țintă</div> | |
| <div class="stat-value" id="target-hours">0</div> | |
| </div> | |
| </div> | |
| <div class="combined-calendar" id="planner-calendar"> | |
| <!-- Days will be dynamically inserted here by JavaScript --> | |
| </div> | |
| <div class="legend"> | |
| <div class="legend-item"> | |
| <div class="legend-color" style="background-color: var(--workday-color);"></div> | |
| <span>Zi lucrată</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="legend-color" style="background-color: var(--holiday-color);"></div> | |
| <span>Sărbătoare/Weekend</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="legend-color" style="background-color: var(--leave-color);"></div> | |
| <span>Concediu</span> | |
| </div> | |
| <div class="legend-item"> | |
| <div class="legend-color" style="background-color: var(--day-off-color);"></div> | |
| <span>Repaus</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Holiday service class | |
| class RomanianHolidays { | |
| constructor() { | |
| this.baseUrl = 'https://date.nager.at/api/v3'; | |
| } | |
| // Get all holidays for a year | |
| async getHolidays(year = new Date().getFullYear()) { | |
| try { | |
| const response = await fetch(`${this.baseUrl}/PublicHolidays/${year}/RO`); | |
| if (!response.ok) throw new Error('Network response was not ok'); | |
| return await response.json(); | |
| } catch (error) { | |
| console.error('Error fetching holidays:', error); | |
| return null; | |
| } | |
| } | |
| // Check if a specific date is a holiday | |
| async isHoliday(date = new Date()) { | |
| const year = date.getFullYear(); | |
| const dateString = date.toISOString().split('T')[0]; // YYYY-MM-DD | |
| const holidays = await this.getHolidays(year); | |
| if (!holidays) return false; | |
| return holidays.some(holiday => holiday.date === dateString); | |
| } | |
| // Get upcoming holidays | |
| async getUpcomingHolidays(count = 5) { | |
| const currentYear = new Date().getFullYear(); | |
| const holidays = await this.getHolidays(currentYear); | |
| if (!holidays) return []; | |
| const today = new Date().toISOString().split('T')[0]; | |
| return holidays | |
| .filter(holiday => holiday.date >= today) | |
| .slice(0, count); | |
| } | |
| // Get holidays for multiple years | |
| async getHolidaysRange(startYear, endYear) { | |
| const years = Array.from({ length: endYear - startYear + 1 }, (_, i) => startYear + i); | |
| const promises = years.map(year => this.getHolidays(year)); | |
| try { | |
| const results = await Promise.all(promises); | |
| return results.flat(); | |
| } catch (error) { | |
| console.error('Error fetching holiday range:', error); | |
| return null; | |
| } | |
| } | |
| } | |
| // Global variables | |
| const monthNamesRo = ["ianuarie","februarie","martie","aprilie","mai","iunie","iulie","august","septembrie","octombrie","noiembrie","decembrie"]; | |
| let currentYear = new Date().getFullYear(); | |
| let selectedYear = currentYear; | |
| let selectedMonth = new Date().getMonth(); | |
| let leaveDays = []; | |
| const refYear = 2017; | |
| const minYear = refYear + 1; | |
| const maxYear = 2037; | |
| // Planner variables | |
| let plannerYear = currentYear; | |
| let plannerMonth = selectedMonth; | |
| let plannerLeaveDays = []; | |
| let plannerWorkedDays = 0; | |
| let plannerTotalHours = 0; | |
| // Initialize holiday service | |
| const holidayService = new RomanianHolidays(); | |
| function handleUrlParams() { | |
| const urlParams = new URLSearchParams(window.location.search); | |
| if (urlParams.has('user')) localStorage.setItem('user', urlParams.get('user')); | |
| if (urlParams.has('tura')) localStorage.setItem('tura', urlParams.get('tura')); | |
| updateUserInfo(); | |
| } | |
| function updateUserInfo() { | |
| const storedUser = localStorage.getItem('user'); | |
| const storedTura = localStorage.getItem('tura'); | |
| const userInfoElement = document.getElementById('userInfo'); | |
| if (storedUser || storedTura) { | |
| let info = 'Configurare: '; | |
| if (storedUser) info += `Utilizator: ${storedUser} `; | |
| if (storedTura) info += `Tură: ${storedTura}`; | |
| userInfoElement.textContent = info; | |
| userInfoElement.style.display = 'block'; | |
| } else { | |
| userInfoElement.textContent = 'Eroare!'; | |
| userInfoElement.style.display = 'block'; | |
| } | |
| } | |
| function getTuraFromUrl() { | |
| let tura = 2; | |
| const urlParams = new URLSearchParams(window.location.search); | |
| if (urlParams.has("tura")) { | |
| tura = parseInt(urlParams.get("tura")); | |
| } else if (urlParams.has("user")) { | |
| const users = ["ljc1q", "xxtoo", "fras0", "l3hb4"]; | |
| const idx = users.indexOf(urlParams.get("user")); | |
| if (idx >= 0) tura = idx + 1; | |
| } else { | |
| const storedTura = localStorage.getItem('tura'); | |
| const storedUser = localStorage.getItem('user'); | |
| if (storedTura) { | |
| tura = parseInt(storedTura); | |
| } else if (storedUser) { | |
| const users = ["ljc1q", "xxtoo", "fras0", "l3hb4"]; | |
| const idx = users.indexOf(storedUser); | |
| if (idx >= 0) tura = idx + 1; | |
| } | |
| } | |
| if (tura % 2 === 0) tura = 6 - tura; | |
| return tura; | |
| } | |
| let plannerShift = getTuraFromUrl(); // Default shift | |
| document.getElementById("shift-planner").textContent = plannerShift; | |
| function isPWA() { | |
| return window.navigator.standalone === true || | |
| window.matchMedia('(display-mode: standalone)').matches || | |
| window.matchMedia('(display-mode: fullscreen)').matches; | |
| } | |
| function createYearButtons() { | |
| const yearButtonsContainer = document.getElementById('yearButtons'); | |
| yearButtonsContainer.innerHTML = ''; | |
| for (let year = currentYear - 3; year <= currentYear + 2; year++) { | |
| const button = document.createElement('button'); | |
| button.className = 'year-btn'; | |
| if (year === selectedYear) button.classList.add('active'); | |
| button.textContent = year; | |
| button.onclick = function () { | |
| selectedYear = year; | |
| plannerYear = year; | |
| saveToLocalStorage(); | |
| updateYearButtons(); | |
| document.getElementById("holidayResult").innerHTML = ``; | |
| updatePlanner(); | |
| }; | |
| yearButtonsContainer.appendChild(button); | |
| } | |
| } | |
| function updateYearButtons() { | |
| const buttons = document.querySelectorAll('.year-btn'); | |
| buttons.forEach(button => { | |
| if (parseInt(button.textContent) === selectedYear) | |
| button.classList.add('active'); | |
| else | |
| button.classList.remove('active'); | |
| }); | |
| } | |
| function populateMonthSelect() { | |
| const monthSelect = document.getElementById('monthSelect'); | |
| monthSelect.innerHTML = ''; | |
| for (let month = 0; month < 12; month++) { | |
| const option = document.createElement('option'); | |
| option.value = month; | |
| option.textContent = monthNamesRo[month]; | |
| if (month === selectedMonth) option.selected = true; | |
| monthSelect.appendChild(option); | |
| } | |
| monthSelect.addEventListener('change', function () { | |
| selectedMonth = parseInt(monthSelect.value); | |
| plannerMonth = selectedMonth; | |
| saveToLocalStorage(); | |
| document.getElementById("holidayResult").innerHTML = ``; | |
| updatePlanner(); | |
| }); | |
| } | |
| function loadFromLocalStorage() { | |
| const storedYear = localStorage.getItem('selectedYear'); | |
| const storedMonth = localStorage.getItem('selectedMonth'); | |
| const storedLeaveDays = localStorage.getItem('leaveDays'); | |
| if (storedYear) selectedYear = parseInt(storedYear); | |
| if (storedMonth) selectedMonth = parseInt(storedMonth); | |
| if (storedLeaveDays) leaveDays = JSON.parse(storedLeaveDays); | |
| // Load planner data | |
| const storedPlannerMonth = localStorage.getItem('plannerMonth'); | |
| const storedPlannerShift = localStorage.getItem('plannerShift'); | |
| const storedPlannerLeaveDays = localStorage.getItem('plannerLeaveDays'); | |
| if (storedPlannerMonth) plannerMonth = parseInt(storedPlannerMonth); | |
| if (storedPlannerShift) plannerShift = parseInt(storedPlannerShift); | |
| if (storedPlannerLeaveDays) plannerLeaveDays = JSON.parse(storedPlannerLeaveDays); | |
| } | |
| function saveToLocalStorage() { | |
| localStorage.setItem('selectedYear', selectedYear); | |
| localStorage.setItem('selectedMonth', selectedMonth); | |
| localStorage.setItem('leaveDays', JSON.stringify(leaveDays)); | |
| // Save planner data | |
| localStorage.setItem('plannerMonth', plannerMonth); | |
| localStorage.setItem('plannerShift', plannerShift); | |
| localStorage.setItem('plannerLeaveDays', JSON.stringify(plannerLeaveDays)); | |
| } | |
| function initializeControls() { | |
| handleUrlParams(); | |
| loadFromLocalStorage(); | |
| createYearButtons(); | |
| populateMonthSelect(); | |
| // Initialize planner controls | |
| document.getElementById('prev-year').addEventListener('click', () => { | |
| plannerYear = plannerYear - 1; | |
| selectedYear = plannerYear; | |
| if (plannerYear < minYear) | |
| plannerYear = minYear; | |
| updateYearButtons(); | |
| updatePlanner(); | |
| }); | |
| document.getElementById('next-year').addEventListener('click', () => { | |
| plannerYear = plannerYear + 1; | |
| selectedYear = plannerYear; | |
| if (plannerYear > maxYear) | |
| plannerYear = maxYear; | |
| updateYearButtons(); | |
| updatePlanner(); | |
| }); | |
| // Initialize planner controls | |
| document.getElementById('prev-month').addEventListener('click', () => { | |
| plannerMonth = (plannerMonth - 1 + 12) % 12; | |
| selectedMonth = plannerMonth; | |
| populateMonthSelect(); | |
| updatePlanner(); | |
| }); | |
| document.getElementById('next-month').addEventListener('click', () => { | |
| plannerMonth = (plannerMonth + 1) % 12; | |
| selectedMonth = plannerMonth; | |
| populateMonthSelect(); | |
| updatePlanner(); | |
| }); | |
| // Initialize optimize button | |
| document.getElementById('optimize-leave').addEventListener('click', optimizeLeaveDays); | |
| if (!isPWA()) { | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const storedUser = localStorage.getItem('user'); | |
| const storedTura = localStorage.getItem('tura'); | |
| let urlChanged = false; | |
| if (storedUser && !urlParams.has('user')) { urlParams.set('user', storedUser); urlChanged = true; } | |
| if (storedTura && !urlParams.has('tura')) { urlParams.set('tura', storedTura); urlChanged = true; } | |
| if (urlChanged) { | |
| const newUrl = `${window.location.pathname}?${urlParams.toString()}`; | |
| window.history.replaceState({}, '', newUrl); | |
| } | |
| } | |
| } | |
| function getEasterDate(year) { | |
| const a = year % 4, b = year % 7, c = year % 19; | |
| const d = (19 * c + 15) % 30; | |
| const e = (2 * a + 4 * b - d + 34) % 7; | |
| const month = Math.floor((d + e + 114) / 31); | |
| const day = ((d + e + 114) % 31) + 1; | |
| let date = new Date(year, month - 1, day); | |
| date.setDate(date.getDate() + 13); | |
| return date; | |
| } | |
| function getDayName(date) { | |
| const days = ['duminică', 'luni', 'marți', 'miercuri', 'joi', 'vineri', 'sâmbătă']; | |
| return days[date.getDay()]; | |
| } | |
| function findClosestWorkShift(holidayDate, tura) { | |
| const date0 = new Date(2024, 0, 1); | |
| let closest = null, minDistance = Infinity; | |
| for (let offset = -10; offset <= 10; offset++) { | |
| let d = new Date(holidayDate); | |
| d.setDate(d.getDate() + offset); | |
| const daysDiff = Math.ceil((d - date0) / 86400000); | |
| const shift = (daysDiff + tura) % 4; | |
| if (shift === 2 || shift === 3) { | |
| let dist = Math.abs(offset); | |
| if (dist < minDistance) { | |
| minDistance = dist; | |
| closest = { | |
| date: d, | |
| shift: shift === 2 ? 'de zi' : 'de noapte', | |
| dayName: getDayName(d) | |
| }; | |
| } | |
| } | |
| } | |
| return closest; | |
| } | |
| function checkHoliday(type) { | |
| const currentDate = new Date(); | |
| const currentYear = currentDate.getFullYear(); | |
| const tura = getTuraFromUrl(); | |
| let date, name = ''; | |
| if (type === "lcrt") { | |
| date = new Date(); | |
| } else if (type === "lless") { | |
| date = new Date(selectedYear, selectedMonth - 1, new Date().getDate()); | |
| } else if (type === "lgrt") { | |
| date = new Date(selectedYear, selectedMonth + 1, new Date().getDate()); | |
| if (date.getFullYear() > currentYear + 2) date = new Date(currentYear + 2, 11, new Date().getDate()); | |
| } else if (type === "lurm") { | |
| date = new Date(currentYear, new Date().getMonth() + 1, 15); | |
| } else if (type === "curm") { | |
| date = new Date(selectedYear, selectedMonth, 15); | |
| if (leaveDays) { | |
| while (true) { | |
| date = new Date(date.getFullYear(), date.getMonth() + 1, 15); | |
| if (date.getFullYear() > currentYear + 2) { | |
| date = new Date(currentYear - 1, 0, 15); | |
| } | |
| if (date.getFullYear() === selectedYear && date.getMonth() === selectedMonth) { | |
| break; | |
| } | |
| const currentMonth = `${date.getFullYear()}-${date.getMonth()}-`; | |
| const currentmlc = leaveDays.filter(day => day.startsWith(currentMonth)).length; | |
| if (currentmlc) break; | |
| } | |
| } | |
| } else { | |
| for (let year = currentYear; year <= currentYear + 10; year++) { | |
| if (type === 'paste') { | |
| date = getEasterDate(year); | |
| name = 'Paște'; | |
| } else if (type === 'craciun') { | |
| date = new Date(year, 11, 25); | |
| name = 'Crăciun'; | |
| } else if (type === 'revelion') { | |
| date = new Date(year, 0, 1); | |
| name = 'Revelion'; | |
| } | |
| if (date > currentDate) break; | |
| } | |
| } | |
| const closest = findClosestWorkShift(date, tura); | |
| if (closest) { | |
| const m = monthNamesRo[closest.date.getMonth()]; | |
| const y = closest.date.getFullYear(); | |
| const d = closest.date.getDate(); | |
| const text = `De ${name} ${y} sunt ${closest.shift} ${closest.dayName}, ${d} ${m} ${y}`; | |
| if (name) { | |
| document.getElementById('holidayResult').textContent = text; | |
| } else { | |
| document.getElementById("holidayResult").innerHTML = ``; | |
| } | |
| selectedYear = y; | |
| plannerYear = y; | |
| selectedMonth = closest.date.getMonth(); | |
| plannerMonth = selectedMonth; | |
| } | |
| saveToLocalStorage(); | |
| updateYearButtons(); | |
| populateMonthSelect(); | |
| updatePlanner(); | |
| } | |
| // Planner functionality | |
| async function updatePlanner() { | |
| const calendar = document.getElementById("planner-calendar"); | |
| const monthDisplay = document.getElementById("current-month"); | |
| const yearDisplay = document.getElementById("current-year"); | |
| const shiftDisplay = document.getElementById("shift-planner"); | |
| const workedDaysDisplay = document.getElementById("worked-days"); | |
| const leaveDaysDisplay = document.getElementById("leave-days"); | |
| const totalHoursDisplay = document.getElementById("total-hours"); | |
| // Update display | |
| monthDisplay.textContent = `${monthNamesRo[plannerMonth]}`; | |
| yearDisplay.textContent = `${plannerYear}`; | |
| plannerShift = getTuraFromUrl(); | |
| if (plannerShift % 2 === 0) { | |
| plannerShift = 6 - plannerShift; | |
| } | |
| shiftDisplay.textContent = plannerShift; | |
| // Get holidays for the current month | |
| const holidays = await getHolidaysForMonth(plannerYear, plannerMonth); | |
| // Calculate days in month | |
| const daysInMonth = new Date(plannerYear, plannerMonth + 1, 0).getDate(); | |
| const firstDay = (new Date(plannerYear, plannerMonth, 1).getDay() + 6) % 7; | |
| let targetHours = 0; | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| if (!holidays.includes(day) && (new Date(plannerYear, plannerMonth, day).getDay() % 6) != 0) | |
| targetHours += 8; | |
| } | |
| document.getElementById("target-hours").textContent = targetHours; | |
| // Reset stats | |
| plannerWorkedDays = 0; | |
| plannerTotalHours = 0; | |
| let currentLeaveDays = 0; | |
| // Clear calendar | |
| calendar.innerHTML = ''; | |
| // Add empty cells for days before the first day of the month | |
| for (let i = 0; i < firstDay; i++) { | |
| const emptyDay = document.createElement("div"); | |
| emptyDay.classList.add("combined-day"); | |
| emptyDay.classList.add("unclickable"); | |
| calendar.appendChild(emptyDay); | |
| } | |
| // Add days of the month | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const dayElement = document.createElement("div"); | |
| dayElement.classList.add("combined-day"); | |
| // Add day number | |
| const dayNumber = document.createElement("div"); | |
| dayNumber.classList.add("day-number"); | |
| dayNumber.textContent = day; | |
| dayElement.appendChild(dayNumber); | |
| // Calculate shift | |
| const date0 = new Date(refYear, 0, 1); | |
| const currentDate = new Date(plannerYear, plannerMonth, day); | |
| const daysDiff = Math.ceil((currentDate - date0) / 86400000); | |
| let shift2 = plannerShift; | |
| if (shift2 % 2 === 0) | |
| shift2 = 6 - shift2; | |
| const shift = (daysDiff + shift2) % 4; | |
| // Add shift info | |
| const shiftInfo = document.createElement("div"); | |
| shiftInfo.classList.add("day-shift"); | |
| let shiftText = ''; | |
| let isWorkDay = false; | |
| if (shift === 0 || shift === 1) { | |
| shiftText = 'Repaus'; | |
| dayElement.classList.add("day-off"); | |
| } else if (shift === 2) { | |
| shiftText = 'Zi'; | |
| dayElement.classList.add("workday"); | |
| isWorkDay = true; | |
| } else if (shift === 3) { | |
| shiftText = 'Noapte'; | |
| dayElement.classList.add("workday"); | |
| isWorkDay = true; | |
| } | |
| shiftInfo.textContent = shiftText; | |
| dayElement.appendChild(shiftInfo); | |
| // Check if holiday | |
| const isHoliday = holidays.includes(day) || | |
| currentDate.getDay() === 0 || // Sunday | |
| currentDate.getDay() === 6; // Saturday | |
| if (isHoliday) { | |
| dayElement.classList.add("holiday"); | |
| } | |
| // Check if leave day | |
| const dateKey = `${plannerYear}-${plannerMonth}-${day}`; | |
| const isLeaveDay = plannerLeaveDays.includes(dateKey); | |
| if (isLeaveDay) { | |
| dayElement.classList.add("leave"); | |
| if (!isHoliday) { | |
| currentLeaveDays++; | |
| } | |
| // Calculate hours for leave day | |
| if (!isHoliday && !isWorkDay) { | |
| plannerTotalHours += 8; // 8 hours for leave on holiday/day off | |
| } else if (!isHoliday && isWorkDay) { | |
| plannerTotalHours += 8; // 8 hours for leave on work day | |
| } | |
| } else if (isWorkDay) { | |
| plannerWorkedDays++; | |
| plannerTotalHours += 12; // 12 hours for work day | |
| } | |
| // Add click event | |
| dayElement.addEventListener('click', () => togglePlannerDay(plannerYear, plannerMonth, day)); | |
| calendar.appendChild(dayElement); | |
| } | |
| // Update stats | |
| workedDaysDisplay.textContent = plannerWorkedDays; | |
| leaveDaysDisplay.textContent = currentLeaveDays; | |
| totalHoursDisplay.textContent = plannerTotalHours; | |
| // Update selected month display | |
| const ldcount = plannerLeaveDays.length; | |
| const currentMonth = `${plannerYear}-${plannerMonth}-`; | |
| const currentmlc = plannerLeaveDays.filter(day => day.startsWith(currentMonth)).length; | |
| const mlc2 = ldcount ? `(${currentmlc}/${ldcount})`: ``; | |
| document.getElementById("selectedMonth").innerHTML = `Calendar ${monthNamesRo[plannerMonth]} ${plannerYear} ${mlc2}`; | |
| // Save to localStorage | |
| saveToLocalStorage(); | |
| } | |
| async function getHolidaysForMonth(year, month) { | |
| try { | |
| const allHolidays = await holidayService.getHolidays(year); | |
| if (!allHolidays) { | |
| console.warn('Could not fetch holidays, using fallback'); | |
| // Return fallback holidays for each month | |
| return getFallbackHolidays(year, month); | |
| } | |
| // Filter holidays that are in the specified month and extract the day | |
| const monthHolidays = allHolidays | |
| .filter(holiday => { | |
| const holidayMonth = parseInt(holiday.date.split('-')[1]); | |
| return holidayMonth === month + 1; // API uses 1-based months | |
| }) | |
| .map(holiday => parseInt(holiday.date.split('-')[2])); | |
| return monthHolidays; | |
| } catch (error) { | |
| console.error('Error getting holidays for month:', error); | |
| return getFallbackHolidays(year, month); | |
| } | |
| } | |
| function getFallbackHolidays(year, month) { | |
| // Fallback holidays for Romania | |
| const holidays = { | |
| 0: [1, 2, 24], // January: 1, 2 (Revelion), 24 (Unirea Principatelor Române) | |
| 3: [], // April: Easter is dynamic, will be handled separately | |
| 4: [1], // May: 1 (Ziua Muncii) | |
| 5: [1], // June: 1 (Ziua Copilului) | |
| 7: [15], // August: 15 (Adormirea Maicii Domnului) | |
| 10: [1, 30], // November: 1 (Ziua Națională), 30 (Sf. Andrei) | |
| 11: [1, 25, 26] // December: 1 (Ziua Națională), 25, 26 (Crăciun) | |
| }; | |
| // Add Easter for April if applicable | |
| if (month === 3) { | |
| const easter = getEasterDate(year); | |
| if (easter.getMonth() === 3) { | |
| holidays[3].push(easter.getDate()); | |
| holidays[3].push(easter.getDate() + 1); // Easter Monday | |
| } | |
| } | |
| // Add Easter for May if applicable (when Easter is in late April) | |
| if (month === 4) { | |
| const easter = getEasterDate(year); | |
| if (easter.getMonth() === 3 && easter.getDate() > 25) { | |
| holidays[4].push(easter.getDate() + 1); // Easter Monday | |
| } | |
| } | |
| return holidays[month] || []; | |
| } | |
| function togglePlannerDay(year, month, day) { | |
| const dateKey = `${year}-${month}-${day}`; | |
| const index = plannerLeaveDays.indexOf(dateKey); | |
| if (index === -1) { | |
| plannerLeaveDays.push(dateKey); | |
| } else { | |
| plannerLeaveDays.splice(index, 1); | |
| } | |
| updatePlanner(); | |
| } | |
| // Schedule optimization algorithm | |
| async function optimizeLeaveDays() { | |
| const resultElement = document.getElementById('optimization-result'); | |
| resultElement.textContent = 'Se calculează...'; | |
| resultElement.className = 'optimization-result calculating'; | |
| try { | |
| const year = plannerYear; | |
| const month = plannerMonth; | |
| // Get holidays for the current month | |
| const holidays = await getHolidaysForMonth(year, month); | |
| // Add weekends to holidays | |
| const daysInMonth = new Date(year, month + 1, 0).getDate(); | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const date = new Date(year, month, day); | |
| if (date.getDay() === 0 || date.getDay() === 6) { // Sunday or Saturday | |
| if (!holidays.includes(day)) { | |
| holidays.push(day); | |
| } | |
| } | |
| } | |
| holidays.sort((a, b) => a - b); | |
| // Calculate target hours | |
| let targetHours = 0; | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| if (!holidays.includes(day) && (new Date(year, month, day).getDay() % 6) !== 0) { | |
| targetHours += 8; | |
| } | |
| } | |
| const refDate = new Date(refYear, 0, 0); | |
| let best = [-Infinity, -Infinity]; | |
| let bestChoice = [-1, -1]; | |
| // Try all possible leave periods | |
| for (let startDay = 1; startDay <= daysInMonth - 1; startDay++) { | |
| for (let endDay = startDay + 1; endDay <= daysInMonth; endDay++) { | |
| let score = [0, 0]; | |
| const leaves = []; | |
| // Create leave period | |
| for (let day = startDay; day <= endDay; day++) { | |
| leaves.push(day); | |
| } | |
| score[1] = leaves.length; | |
| let countHours = 0; | |
| // Calculate hours for this configuration | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const date1 = new Date(year, month, day); | |
| const ecart = Math.round((date1 - refDate) / 86400000) + 7 - plannerShift; | |
| // Check if it's a work day and not on leave | |
| if (((ecart % 4) < 2) && !leaves.includes(day)) { | |
| countHours += 12; | |
| } else if (!holidays.includes(day) && leaves.includes(day)) { | |
| // Leave day on regular day | |
| countHours += 8; | |
| } | |
| // Holidays and weekends don't contribute to hours | |
| } | |
| // Only consider configurations that match target hours | |
| if (countHours === targetHours) { | |
| // Calculate score: count work days that fall on holidays | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const date1 = new Date(year, month, day); | |
| const ecart = Math.round((date1 - refDate) / 86400000) + 7 - plannerShift; | |
| // If it's a holiday, a work day, and not on leave | |
| if (holidays.includes(day) && ((ecart % 4) < 2) && !leaves.includes(day)) { | |
| score[0] += 1; | |
| } | |
| } | |
| } | |
| // Update best choice | |
| if (score[0] > best[0] || (score[0] === best[0] && score[1] > best[1])) { | |
| best[0] = score[0]; | |
| best[1] = score[1]; | |
| bestChoice[0] = startDay; | |
| bestChoice[1] = endDay; | |
| } | |
| } | |
| } | |
| // Apply the best leave period | |
| if (bestChoice[0] !== -1 && bestChoice[1] !== -1) { | |
| // Clear existing leave days for this month | |
| plannerLeaveDays = plannerLeaveDays.filter(day => { | |
| const [y, m, d] = day.split('-').map(Number); | |
| return !(y === year && m === month); | |
| }); | |
| // Add new leave days | |
| for (let day = bestChoice[0]; day <= bestChoice[1]; day++) { | |
| const dateKey = `${year}-${month}-${day}`; | |
| plannerLeaveDays.push(dateKey); | |
| } | |
| // Update the planner | |
| updatePlanner(); | |
| resultElement.textContent = `Concediu optimizat: zilele ${bestChoice[0]}-${bestChoice[1]} ${monthNamesRo[month]}`; | |
| resultElement.className = 'optimization-result success'; | |
| } else { | |
| resultElement.textContent = 'Nu s-a găsit o soluție optimă'; | |
| resultElement.className = 'optimization-result error'; | |
| } | |
| } catch (error) { | |
| console.error('Error optimizing leave days:', error); | |
| resultElement.textContent = 'Eroare la optimizare'; | |
| resultElement.className = 'optimization-result error'; | |
| } | |
| } | |
| // Initialize the app | |
| window.addEventListener('load', function() { | |
| initializeControls(); | |
| updatePlanner(); | |
| }); | |
| document.addEventListener('visibilitychange', function() { | |
| if (!document.hidden) updateUserInfo(); | |
| }); | |
| if ("serviceWorker" in navigator) { | |
| window.addEventListener('load', function() { | |
| navigator.serviceWorker.register("sw.js"); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |