Enhance calendar functionality with start date management and excluded month visibility. Added start date state and date picker for user-defined start dates. Updated holiday and PTO calculations to respect the start date. Improved UI to toggle excluded months and display active months based on the start date. Refactored related utility functions for better date handling.
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>;
|
||||
export let selectedCountryCode: string;
|
||||
export let weekendDays: number[] = [6, 0];
|
||||
export let startDate: Date = new Date(year, 0, 1);
|
||||
export let isActive: boolean = true;
|
||||
|
||||
// Function to determine the first day of the week based on locale
|
||||
function getFirstDayOfWeek(locale: string): number {
|
||||
@@ -84,12 +86,19 @@
|
||||
return weekendDays.includes(date.getDay());
|
||||
}
|
||||
|
||||
function isPastDate(day: number): boolean {
|
||||
const date = new Date(year, month, day);
|
||||
// Normalize startDate to current year for comparison
|
||||
const startDateInYear = new Date(year, startDate.getMonth(), startDate.getDate());
|
||||
return date < startDateInYear;
|
||||
}
|
||||
|
||||
const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||
|
||||
$: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek));
|
||||
</script>
|
||||
|
||||
<div class="calendar">
|
||||
<div class="calendar {isActive ? '' : 'excluded-month'}">
|
||||
<div class="month-name">{new Date(year, month).toLocaleString('default', { month: 'long' })}</div>
|
||||
|
||||
{#each orderedDayInitials as dayInitial}
|
||||
@@ -101,7 +110,8 @@
|
||||
{/each}
|
||||
{#each Array.from({ length: daysInMonth }, (_, i) => i + 1) as day}
|
||||
{@const holiday = getHoliday(day)}
|
||||
<div class="day {isWeekend(new Date(year, month, day)) ? 'weekend' : ''} {holiday ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}">
|
||||
{@const pastDate = isPastDate(day)}
|
||||
<div class="day {isWeekend(new Date(year, month, day)) ? 'weekend' : ''} {holiday ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''} {pastDate ? 'past-date' : ''}">
|
||||
<span class={holiday?.hidden ? 'strikethrough' : ''}>{day}</span>
|
||||
{#if holiday}
|
||||
<Tooltip text={holiday.name} />
|
||||
@@ -137,6 +147,14 @@
|
||||
color: #c5c6c7;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.excluded-month .month-name {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.excluded-month .day-initial {
|
||||
color: #666;
|
||||
}
|
||||
.day {
|
||||
aspect-ratio: 1;
|
||||
text-align: center;
|
||||
@@ -202,4 +220,12 @@
|
||||
text-decoration: line-through;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.past-date {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.past-date span {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
@@ -31,49 +31,56 @@ export function getHolidaysForYear(countryCode: string, year: number, stateCode?
|
||||
}
|
||||
|
||||
// Find optimal placement of PTO days to maximize consecutive time off
|
||||
export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6]): Date[] {
|
||||
export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number, weekendDays: number[] = [0, 6], startDate?: Date): Date[] {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const filteredHolidays = holidays.filter(h => h.date.getFullYear() === year && h.date >= effectiveStartDate);
|
||||
const allDaysOff = new Set([
|
||||
...holidays.filter(h => h.date.getFullYear() === year).map(h => dateKey(h.date)),
|
||||
...getWeekends(year, weekendDays).map(d => dateKey(d))
|
||||
...filteredHolidays.map(h => dateKey(h.date)),
|
||||
...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d))
|
||||
]);
|
||||
|
||||
const gaps = findGaps(allDaysOff, year, weekendDays);
|
||||
const gaps = findGaps(allDaysOff, year, weekendDays, effectiveStartDate);
|
||||
return selectDaysOff(rankGapsByEfficiency(gaps, allDaysOff, weekendDays), daysOff, allDaysOff, weekendDays);
|
||||
}
|
||||
|
||||
// Calculate periods of consecutive days off (weekends + holidays + PTO)
|
||||
export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6]) {
|
||||
export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number, weekendDays: number[] = [0, 6], startDate?: Date) {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const filteredHolidays = holidays.filter(h => h.date >= effectiveStartDate);
|
||||
const filteredOptimizedDaysOff = optimizedDaysOff.filter(d => d >= effectiveStartDate);
|
||||
|
||||
const allDaysOff = new Set([
|
||||
...holidays.map(h => dateKey(h.date)),
|
||||
...optimizedDaysOff.map(d => dateKey(d)),
|
||||
...getWeekends(year, weekendDays).map(d => dateKey(d))
|
||||
...filteredHolidays.map(h => dateKey(h.date)),
|
||||
...filteredOptimizedDaysOff.map(d => dateKey(d)),
|
||||
...getWeekends(year, weekendDays, effectiveStartDate).map(d => dateKey(d))
|
||||
]);
|
||||
|
||||
const consecutiveDaysOff = [];
|
||||
let currentGroup = [];
|
||||
|
||||
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (isWeekend(d, weekendDays) || isHoliday(d, holidays) || allDaysOff.has(dateKey(d))) {
|
||||
for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (isWeekend(d, weekendDays) || isHoliday(d, filteredHolidays) || allDaysOff.has(dateKey(d))) {
|
||||
currentGroup.push(new Date(d));
|
||||
} else if (currentGroup.length > 0) {
|
||||
if (isValidConsecutiveGroup(currentGroup, weekendDays)) {
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff));
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff));
|
||||
}
|
||||
currentGroup = [];
|
||||
}
|
||||
}
|
||||
|
||||
if (currentGroup.length > 0 && isValidConsecutiveGroup(currentGroup, weekendDays)) {
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, optimizedDaysOff));
|
||||
consecutiveDaysOff.push(createPeriod(currentGroup, filteredOptimizedDaysOff));
|
||||
}
|
||||
|
||||
return consecutiveDaysOff;
|
||||
}
|
||||
|
||||
// Get all weekend days for a year
|
||||
function getWeekends(year: number, weekendDays: number[]): Date[] {
|
||||
function getWeekends(year: number, weekendDays: number[], startDate?: Date): Date[] {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const weekends = [];
|
||||
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (d.getMonth() === d.getMonth() && isWeekend(d, weekendDays)) {
|
||||
weekends.push(new Date(d));
|
||||
}
|
||||
@@ -82,11 +89,12 @@ function getWeekends(year: number, weekendDays: number[]): Date[] {
|
||||
}
|
||||
|
||||
// Find gaps between days off that could be filled with PTO
|
||||
function findGaps(allDaysOff: Set<string>, year: number, weekendDays: number[]) {
|
||||
function findGaps(allDaysOff: Set<string>, year: number, weekendDays: number[], startDate?: Date) {
|
||||
const effectiveStartDate = startDate || new Date(year, 0, 1);
|
||||
const gaps = [];
|
||||
let gapStart = null;
|
||||
|
||||
for (let d = new Date(year, 0, 1); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
for (let d = new Date(effectiveStartDate); d <= new Date(year, 11, 31); d.setDate(d.getDate() + 1)) {
|
||||
if (!allDaysOff.has(dateKey(d)) && !isWeekend(d, weekendDays)) {
|
||||
if (!gapStart) gapStart = new Date(d);
|
||||
} else if (gapStart) {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
let daysOff: number = 0;
|
||||
let optimizedDaysOff: Date[] = [];
|
||||
let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }> = [];
|
||||
let showExcludedMonths: boolean = true;
|
||||
let visibleMonths: number[] = [];
|
||||
let countriesInput: HTMLInputElement | null = null;
|
||||
let statesInput: HTMLInputElement | null = null;
|
||||
let showHowItWorks: boolean = false;
|
||||
@@ -35,12 +37,27 @@
|
||||
let showWeekendSettings: boolean = false;
|
||||
let weekendDays: number[] = [];
|
||||
|
||||
// Start date state
|
||||
let startDate: Date = new Date(new Date().getFullYear(), 0, 1);
|
||||
let showDatePicker: boolean = false;
|
||||
let datePickerValue: string = '';
|
||||
|
||||
$: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
|
||||
|
||||
$: if (selectedCountryCode || selectedStateCode || daysOff || year) {
|
||||
$: if (selectedCountryCode || selectedStateCode || daysOff || year || startDate) {
|
||||
updateHolidays();
|
||||
}
|
||||
|
||||
// Reactive: when year changes, load start date for that year
|
||||
$: if (year !== undefined && year) {
|
||||
startDate = getStartDate(year);
|
||||
}
|
||||
|
||||
// Reactive: when startDate or year changes, update excluded months visibility
|
||||
$: if (year !== undefined && year && startDate) {
|
||||
showExcludedMonths = !hasExcludedMonths();
|
||||
}
|
||||
|
||||
$: if (daysOff) {
|
||||
localStorage.setItem('daysOff', daysOff.toString());
|
||||
}
|
||||
@@ -59,8 +76,11 @@
|
||||
const stateName = target.value;
|
||||
selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName) || '';
|
||||
selectedState = stateName;
|
||||
localStorage.setItem('selectedState', selectedState);
|
||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||
// Save state per country
|
||||
if (selectedCountryCode) {
|
||||
localStorage.setItem(`selectedState_${selectedCountryCode}`, selectedState);
|
||||
localStorage.setItem(`selectedStateCode_${selectedCountryCode}`, selectedStateCode);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -73,14 +93,22 @@
|
||||
const storedYear = localStorage.getItem('year');
|
||||
const storedCountry = localStorage.getItem('selectedCountry');
|
||||
const storedDaysOff = localStorage.getItem('daysOff');
|
||||
const storedState = localStorage.getItem('selectedState');
|
||||
const storedStateCode = localStorage.getItem('selectedStateCode');
|
||||
|
||||
year = storedYear ? parseInt(storedYear, 10) : defaultYear;
|
||||
selectedCountry = storedCountry || defaultCountry;
|
||||
daysOff = storedDaysOff ? parseInt(storedDaysOff, 10) : defaultDaysOff;
|
||||
selectedState = storedState || '';
|
||||
selectedStateCode = storedStateCode || '';
|
||||
|
||||
// Load state per country
|
||||
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
|
||||
if (countryCode) {
|
||||
selectedState = localStorage.getItem(`selectedState_${countryCode}`) || '';
|
||||
selectedStateCode = localStorage.getItem(`selectedStateCode_${countryCode}`) || '';
|
||||
} else {
|
||||
selectedState = '';
|
||||
selectedStateCode = '';
|
||||
}
|
||||
startDate = getStartDate(year);
|
||||
// showExcludedMonths will be set by reactive statement
|
||||
updateHolidays();
|
||||
});
|
||||
|
||||
@@ -113,14 +141,17 @@
|
||||
function handleCountryChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const fullValue = target.value;
|
||||
if (selectedCountryCode) {
|
||||
daysOff = ptoData[selectedCountryCode] || 0;
|
||||
selectedState = ''; // Reset state
|
||||
selectedStateCode = ''; // Reset state code
|
||||
updateStatesList(selectedCountryCode); // Update states list for the new country
|
||||
selectedCountry = fullValue;
|
||||
// Get the country code for the new country (selectedCountryCode will update reactively)
|
||||
const newCountryCode = Object.keys(countriesList).find(code => countriesList[code] === fullValue) || '';
|
||||
if (newCountryCode) {
|
||||
// Update days off to the new country's default
|
||||
daysOff = ptoData[newCountryCode] || 0;
|
||||
// Load state for the new country
|
||||
selectedState = localStorage.getItem(`selectedState_${newCountryCode}`) || '';
|
||||
selectedStateCode = localStorage.getItem(`selectedStateCode_${newCountryCode}`) || '';
|
||||
// updateStatesList and updateHolidays will be called by reactive statements
|
||||
localStorage.setItem('selectedCountry', selectedCountry);
|
||||
localStorage.setItem('selectedState', selectedState);
|
||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||
localStorage.setItem('daysOff', daysOff.toString());
|
||||
}
|
||||
}
|
||||
@@ -135,8 +166,8 @@
|
||||
hidden: isHolidayHidden(holiday)
|
||||
}));
|
||||
const visibleHolidays = holidays.filter(h => !h.hidden);
|
||||
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays);
|
||||
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays);
|
||||
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff, weekendDays, startDate);
|
||||
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year, weekendDays, startDate);
|
||||
} else {
|
||||
holidays = [];
|
||||
optimizedDaysOff = [];
|
||||
@@ -147,14 +178,18 @@
|
||||
function resetToDefault() {
|
||||
year = defaultYear;
|
||||
selectedCountry = defaultCountry;
|
||||
const defaultCountryCode = Object.keys(countriesList).find(code => countriesList[code] === defaultCountry) || '';
|
||||
// Load state for default country
|
||||
if (defaultCountryCode) {
|
||||
selectedState = localStorage.getItem(`selectedState_${defaultCountryCode}`) || '';
|
||||
selectedStateCode = localStorage.getItem(`selectedStateCode_${defaultCountryCode}`) || '';
|
||||
} else {
|
||||
selectedState = '';
|
||||
selectedStateCode = '';
|
||||
daysOff = defaultDaysOff;
|
||||
}
|
||||
// Keep current daysOff value, don't reset it
|
||||
localStorage.setItem('year', year.toString());
|
||||
localStorage.setItem('selectedCountry', selectedCountry);
|
||||
localStorage.setItem('selectedState', selectedState);
|
||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||
localStorage.setItem('daysOff', daysOff.toString());
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
@@ -252,6 +287,156 @@
|
||||
|
||||
$: visibleHolidaysCount = holidays.filter(h => !h.hidden).length;
|
||||
|
||||
// Get start date for a given year from localStorage
|
||||
function getStartDate(year: number): Date {
|
||||
try {
|
||||
const stored = localStorage.getItem('startDates');
|
||||
if (stored) {
|
||||
const startDates: string[] = JSON.parse(stored);
|
||||
// Find date that matches the year (extract year from date string)
|
||||
const dateStr = startDates.find(date => {
|
||||
const dateYear = parseInt(date.split('-')[0] || date.split('T')[0].split('-')[0]);
|
||||
return dateYear === year;
|
||||
});
|
||||
if (dateStr) {
|
||||
// Parse date string - handle both YYYY-MM-DD and ISO format
|
||||
const parsed = dateStr.includes('T') ? dateStr.split('T')[0] : dateStr;
|
||||
const [y, m, d] = parsed.split('-').map(Number);
|
||||
return new Date(y, m - 1, d);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error loading start date:', e);
|
||||
}
|
||||
return new Date(year, 0, 1); // Default to Jan 1st
|
||||
}
|
||||
|
||||
// Save start date for a given year to localStorage
|
||||
function saveStartDate(year: number, date: Date) {
|
||||
try {
|
||||
const stored = localStorage.getItem('startDates');
|
||||
let startDates: string[] = stored ? JSON.parse(stored) : [];
|
||||
// Remove existing date for this year (extract year from date string)
|
||||
startDates = startDates.filter(dateStr => {
|
||||
const dateYear = parseInt(dateStr.split('-')[0] || dateStr.split('T')[0].split('-')[0]);
|
||||
return dateYear !== year;
|
||||
});
|
||||
// Add new date
|
||||
startDates.push(formatDateForInput(date));
|
||||
localStorage.setItem('startDates', JSON.stringify(startDates));
|
||||
} catch (e) {
|
||||
console.error('Error saving start date:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Format date as YYYY-MM-DD for date input (no timezone conversion)
|
||||
function formatDateForInput(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
// Format start date for display
|
||||
function formatStartDate(date: Date): string {
|
||||
const today = new Date();
|
||||
if (date.getTime() === new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime() && date.getFullYear() === year) {
|
||||
return 'Today';
|
||||
}
|
||||
const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const day = date.getDate();
|
||||
const suffix = getDaySuffix(day);
|
||||
return `${monthNames[date.getMonth()]} ${day}${suffix}`;
|
||||
}
|
||||
|
||||
// Get day suffix (st, nd, rd, th)
|
||||
function getDaySuffix(day: number): string {
|
||||
if (day > 3 && day < 21) return 'th';
|
||||
switch (day % 10) {
|
||||
case 1: return 'st';
|
||||
case 2: return 'nd';
|
||||
case 3: return 'rd';
|
||||
default: return 'th';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle start date change
|
||||
function handleStartDateChange(newDate: Date) {
|
||||
startDate = newDate;
|
||||
saveStartDate(year, newDate);
|
||||
updateHolidays();
|
||||
// showExcludedMonths will be updated by reactive statement
|
||||
}
|
||||
|
||||
// Handle date picker input change (auto-save)
|
||||
function handleDatePickerChange() {
|
||||
if (datePickerValue) {
|
||||
// Parse YYYY-MM-DD format in local time (consistent with getStartDate)
|
||||
const [y, m, d] = datePickerValue.split('-').map(Number);
|
||||
const newDate = new Date(y, m - 1, d);
|
||||
handleStartDateChange(newDate);
|
||||
}
|
||||
}
|
||||
|
||||
// Set start date to today
|
||||
function setStartDateToToday() {
|
||||
const today = new Date();
|
||||
if (today.getFullYear() === year) {
|
||||
handleStartDateChange(new Date(today.getFullYear(), today.getMonth(), today.getDate()));
|
||||
showDatePicker = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset start date to Jan 1st
|
||||
function resetStartDateToJan1() {
|
||||
const jan1st = new Date(year, 0, 1);
|
||||
handleStartDateChange(jan1st);
|
||||
showDatePicker = false;
|
||||
}
|
||||
|
||||
// Check if today is in the current year
|
||||
function isTodayInYear(): boolean {
|
||||
const today = new Date();
|
||||
return today.getFullYear() === year;
|
||||
}
|
||||
|
||||
// Helper: Get start date normalized to the current year
|
||||
function getStartDateInYear(): Date {
|
||||
return new Date(year, startDate.getMonth(), startDate.getDate());
|
||||
}
|
||||
|
||||
// Check if a month is active (not entirely before the start date)
|
||||
function isMonthActive(monthIndex: number): boolean {
|
||||
const startDateInYear = getStartDateInYear();
|
||||
const startMonth = startDateInYear.getMonth();
|
||||
|
||||
// Month is active if:
|
||||
// 1. The start date falls within this month (same month), OR
|
||||
// 2. The month starts on or after the start date (later month)
|
||||
// This means only months entirely before the start date's month are excluded
|
||||
return monthIndex >= startMonth;
|
||||
}
|
||||
|
||||
// Check if start date is Jan 1st
|
||||
function isStartDateJan1st(): boolean {
|
||||
const startDateInYear = getStartDateInYear();
|
||||
return startDateInYear.getTime() === new Date(year, 0, 1).getTime();
|
||||
}
|
||||
|
||||
// Check if there are any excluded months (months entirely before the start date)
|
||||
function hasExcludedMonths(): boolean {
|
||||
return months.some(month => !isMonthActive(month));
|
||||
}
|
||||
|
||||
// Filter months based on showExcludedMonths setting
|
||||
// Explicitly depend on startDate, year, and showExcludedMonths to ensure proper reactivity
|
||||
// Use a computed value that depends on all relevant variables
|
||||
$: visibleMonths = (startDate && year !== undefined && year)
|
||||
? (showExcludedMonths
|
||||
? months
|
||||
: months.filter(month => isMonthActive(month)))
|
||||
: [];
|
||||
|
||||
function toggleWeekendDay(dayNumber: number) {
|
||||
if (weekendDays.includes(dayNumber)) {
|
||||
weekendDays = weekendDays.filter(d => d !== dayNumber);
|
||||
@@ -318,7 +503,7 @@
|
||||
|
||||
.content-box p {
|
||||
text-align: center;
|
||||
line-height: 2;
|
||||
line-height: 3;
|
||||
}
|
||||
|
||||
input {
|
||||
@@ -365,6 +550,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-excluded-months-container {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.toggle-excluded-months {
|
||||
padding: 8px 16px;
|
||||
background-color: #333;
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: background-color 0.3s;
|
||||
width: auto;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toggle-excluded-months:hover {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
@@ -557,7 +765,6 @@
|
||||
}
|
||||
|
||||
.content-box button {
|
||||
margin-left: 10px;
|
||||
background-color: #444;
|
||||
border: none;
|
||||
color: #fff;
|
||||
@@ -641,6 +848,161 @@
|
||||
margin-bottom: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.start-date-link {
|
||||
text-decoration: underline;
|
||||
text-decoration-style: dotted;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.start-date-link:hover {
|
||||
text-decoration-style: solid;
|
||||
}
|
||||
|
||||
.date-picker-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.date-picker-modal {
|
||||
background-color: #222;
|
||||
border-radius: 10px;
|
||||
padding: 25px;
|
||||
max-width: min(400px, calc(100vw - 40px));
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-picker-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.date-picker-close:hover,
|
||||
.date-picker-close:active,
|
||||
.date-picker-close:focus {
|
||||
background: transparent;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.date-picker-modal h3 {
|
||||
margin: 0 0 10px 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.date-picker-modal p {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.date-picker-controls {
|
||||
margin: 0 0 20px 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.date-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 1em;
|
||||
background-color: #222;
|
||||
border: 1px solid #555;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.date-input:focus {
|
||||
outline: 2px solid #61dafb;
|
||||
border-color: #61dafb;
|
||||
}
|
||||
|
||||
/* Make calendar icon white */
|
||||
.date-input::-webkit-calendar-picker-indicator {
|
||||
filter: invert(1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.date-input::-webkit-inner-spin-button,
|
||||
.date-input::-webkit-clear-button {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.date-picker-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
margin: 0 0 15px 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.date-picker-button,
|
||||
.date-picker-save {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.date-picker-button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
background-color: #444;
|
||||
color: #fff;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.date-picker-button:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
.date-picker-save {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
background-color: #61dafb;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.date-picker-save:hover {
|
||||
background-color: #4fa8c5;
|
||||
}
|
||||
</style>
|
||||
|
||||
<main>
|
||||
@@ -689,15 +1051,27 @@
|
||||
<span class="bold">{daysOff}</span>
|
||||
<button on:click={() => { daysOff++; updateHolidays(); }} aria-label="Increase days off">▲</button>
|
||||
</span>
|
||||
days off in
|
||||
days off from
|
||||
<a href="#" on:click|preventDefault={() => { showDatePicker = true; datePickerValue = formatDateForInput(startDate); }} class="bold start-date-link">
|
||||
{@html formatStartDate(startDate)}
|
||||
</a>
|
||||
until the end of
|
||||
<span class="arrow-controls">
|
||||
<button on:click={() => { year--; updateHolidays(); }} aria-label="Previous year">◀</button>
|
||||
<span class="bold">{year}</span>
|
||||
<button on:click={() => { year++; updateHolidays(); }} aria-label="Next year">▶</button>
|
||||
</span>
|
||||
</p>
|
||||
{#if year !== defaultYear || selectedCountry !== defaultCountry || daysOff !== defaultDaysOff}
|
||||
<a href="#" on:click|preventDefault={resetToDefault} class="reset-link">Reset to my country</a>
|
||||
{#if year !== defaultYear || selectedCountry !== defaultCountry}
|
||||
{@const yearDifferent = year !== defaultYear}
|
||||
{@const countryDifferent = selectedCountry !== defaultCountry}
|
||||
<a href="#" on:click|preventDefault={resetToDefault} class="reset-link">
|
||||
{yearDifferent && countryDifferent
|
||||
? 'Reset to current country and year'
|
||||
: yearDifferent
|
||||
? 'Reset to current year'
|
||||
: 'Reset to current country'}
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
<datalist id="countries">
|
||||
@@ -781,13 +1155,27 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasExcludedMonths()}
|
||||
<div class="toggle-excluded-months-container">
|
||||
<button
|
||||
class="toggle-excluded-months"
|
||||
on:click={() => showExcludedMonths = !showExcludedMonths}
|
||||
aria-label={showExcludedMonths ? 'Hide excluded months' : 'Show excluded months'}
|
||||
>
|
||||
{showExcludedMonths ? 'Hide' : 'Show'} excluded months
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="calendar-grid">
|
||||
{#each months as month}
|
||||
{#each visibleMonths as month}
|
||||
<div class="calendar-container">
|
||||
<CalendarMonth
|
||||
year={year}
|
||||
month={month}
|
||||
holidays={holidays}
|
||||
startDate={startDate}
|
||||
isActive={isMonthActive(month)}
|
||||
optimizedDaysOff={optimizedDaysOff}
|
||||
consecutiveDaysOff={consecutiveDaysOff}
|
||||
selectedCountryCode={selectedCountryCode}
|
||||
@@ -798,6 +1186,41 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showDatePicker}
|
||||
<div class="date-picker-overlay" on:click|self={() => showDatePicker = false}>
|
||||
<div class="date-picker-modal" on:click|stopPropagation>
|
||||
<button class="date-picker-close" on:click={() => showDatePicker = false} aria-label="Close">×</button>
|
||||
<h3>Set Start Date</h3>
|
||||
<p>Choose when your time off period begins for {year}</p>
|
||||
<div class="date-picker-controls">
|
||||
<input
|
||||
type="date"
|
||||
bind:value={datePickerValue}
|
||||
on:change={handleDatePickerChange}
|
||||
class="date-input"
|
||||
min={formatDateForInput(new Date(year, 0, 1))}
|
||||
max={formatDateForInput(new Date(year, 11, 31))}
|
||||
/>
|
||||
</div>
|
||||
<div class="date-picker-buttons">
|
||||
{#if isTodayInYear()}
|
||||
<button on:click={setStartDateToToday} class="date-picker-button">
|
||||
Set to today
|
||||
</button>
|
||||
{/if}
|
||||
{#if !isStartDateJan1st()}
|
||||
<button on:click={resetStartDateToJan1} class="date-picker-button">
|
||||
Reset to Jan 1st
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="date-picker-button date-picker-save" on:click={() => showDatePicker = false}>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button type="button" class="toggle-text" on:click={toggleHowItWorks}>
|
||||
{showHowItWorks ? 'Hide Details' : 'How does this work?'}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user