ture / index.html
Danielzapirtan's picture
m
60c0dd6
<!DOCTYPE html>
<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">&lt;</button>
<span id="current-month">Ianuarie</span>
<button class="nav-btn" id="next-month">&gt;</button>
</div>
<div class="year-navigation">
<button class="nav-btn" id="prev-year">&lt;</button>
<span id="current-year">2024</span>
<button class="nav-btn" id="next-year">&gt;</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&lt</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&gt</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>