Fix most errors and warnings with GPT, switch to .ts etc
This commit is contained in:
@@ -1,16 +1,15 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import Tooltip from './Tooltip.svelte';
|
import Tooltip from './Tooltip.svelte';
|
||||||
|
|
||||||
export let year;
|
export let year: number;
|
||||||
export let month;
|
export let month: number;
|
||||||
export let holidays = [];
|
export let holidays: Array<{ date: Date; name: string; hidden?: boolean }>;
|
||||||
export let optimizedDaysOff = [];
|
export let optimizedDaysOff: Date[];
|
||||||
export let consecutiveDaysOff = [];
|
export let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }>;
|
||||||
export let selectedCountryCode;
|
export let selectedCountryCode: string;
|
||||||
|
|
||||||
// Function to determine the first day of the week based on locale
|
// Function to determine the first day of the week based on locale
|
||||||
function getFirstDayOfWeek(locale) {
|
function getFirstDayOfWeek(locale: string): number {
|
||||||
// Convert 'us' to proper locale format
|
|
||||||
const normalizedLocale = locale.toLowerCase() === 'us' ? 'en-US' : `en-${locale.toUpperCase()}`;
|
const normalizedLocale = locale.toLowerCase() === 'us' ? 'en-US' : `en-${locale.toUpperCase()}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -34,15 +33,15 @@
|
|||||||
$: firstDayOfWeek = getFirstDayOfWeek(locale);
|
$: firstDayOfWeek = getFirstDayOfWeek(locale);
|
||||||
$: adjustedFirstDay = (getFirstDayOfMonth(year, month) - firstDayOfWeek + 7) % 7;
|
$: adjustedFirstDay = (getFirstDayOfMonth(year, month) - firstDayOfWeek + 7) % 7;
|
||||||
|
|
||||||
function getDaysInMonth(year, month) {
|
function getDaysInMonth(year: number, month: number): number {
|
||||||
return new Date(year, month + 1, 0).getDate();
|
return new Date(year, month + 1, 0).getDate();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFirstDayOfMonth(year, month) {
|
function getFirstDayOfMonth(year: number, month: number): number {
|
||||||
return new Date(year, month, 1).getDay();
|
return new Date(year, month, 1).getDay();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHoliday(day) {
|
function getHoliday(day: number): { date: Date; name: string; hidden?: boolean } | undefined {
|
||||||
return holidays.find(holiday =>
|
return holidays.find(holiday =>
|
||||||
holiday.date.getFullYear() === year &&
|
holiday.date.getFullYear() === year &&
|
||||||
holiday.date.getMonth() === month &&
|
holiday.date.getMonth() === month &&
|
||||||
@@ -50,7 +49,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOptimizedDayOff(day) {
|
function isOptimizedDayOff(day: number): boolean {
|
||||||
return optimizedDaysOff.some(date =>
|
return optimizedDaysOff.some(date =>
|
||||||
date.getFullYear() === year &&
|
date.getFullYear() === year &&
|
||||||
date.getMonth() === month &&
|
date.getMonth() === month &&
|
||||||
@@ -58,8 +57,7 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the dominant month for each consecutive days off period
|
function getDominantMonth(period: { startDate: Date; endDate: Date }): number {
|
||||||
function getDominantMonth(period) {
|
|
||||||
const startMonth = period.startDate.getMonth();
|
const startMonth = period.startDate.getMonth();
|
||||||
const endMonth = period.endDate.getMonth();
|
const endMonth = period.endDate.getMonth();
|
||||||
|
|
||||||
@@ -73,7 +71,7 @@
|
|||||||
return startDays > endDays ? startMonth : endMonth;
|
return startDays > endDays ? startMonth : endMonth;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isConsecutiveDayOff(day) {
|
function isConsecutiveDayOff(day: number): boolean {
|
||||||
return consecutiveDaysOff.some(period => {
|
return consecutiveDaysOff.some(period => {
|
||||||
const start = period.startDate;
|
const start = period.startDate;
|
||||||
const end = period.endDate;
|
const end = period.endDate;
|
||||||
@@ -82,26 +80,21 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to determine if a day is a weekend
|
function isWeekend(day: number): boolean {
|
||||||
function isWeekend(day) {
|
|
||||||
const dayOfWeek = (adjustedFirstDay + day - 1) % 7;
|
const dayOfWeek = (adjustedFirstDay + day - 1) % 7;
|
||||||
// Calculate the indices for Saturday and Sunday
|
|
||||||
const saturdayIndex = (6 - firstDayOfWeek + 7) % 7;
|
const saturdayIndex = (6 - firstDayOfWeek + 7) % 7;
|
||||||
const sundayIndex = (7 - firstDayOfWeek + 7) % 7;
|
const sundayIndex = (7 - firstDayOfWeek + 7) % 7;
|
||||||
return dayOfWeek === saturdayIndex || dayOfWeek === sundayIndex;
|
return dayOfWeek === saturdayIndex || dayOfWeek === sundayIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed array of day initials starting from Sunday
|
|
||||||
const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
const dayInitials = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
|
||||||
|
|
||||||
// Reactive declaration to get the ordered day initials
|
|
||||||
$: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek));
|
$: orderedDayInitials = dayInitials.slice(firstDayOfWeek).concat(dayInitials.slice(0, firstDayOfWeek));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="calendar">
|
<div class="calendar">
|
||||||
<div class="month-name">{new Date(year, month).toLocaleString('default', { month: 'long' })}</div>
|
<div class="month-name">{new Date(year, month).toLocaleString('default', { month: 'long' })}</div>
|
||||||
|
|
||||||
<!-- Day initials header -->
|
|
||||||
{#each orderedDayInitials as dayInitial}
|
{#each orderedDayInitials as dayInitial}
|
||||||
<div class="day-initial">{dayInitial}</div>
|
<div class="day-initial">{dayInitial}</div>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -113,7 +106,7 @@
|
|||||||
<div class="day {isWeekend(day) ? 'weekend' : ''} {getHoliday(day) ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}">
|
<div class="day {isWeekend(day) ? 'weekend' : ''} {getHoliday(day) ? 'holiday' : ''} {isOptimizedDayOff(day) ? 'optimized' : ''} {isConsecutiveDayOff(day) ? 'consecutive-day' : ''}">
|
||||||
{day}
|
{day}
|
||||||
{#if getHoliday(day)}
|
{#if getHoliday(day)}
|
||||||
<Tooltip text={getHoliday(day).name} />
|
<Tooltip text={getHoliday(day)?.name} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -5,32 +5,32 @@ const MS_IN_A_DAY = 86400000;
|
|||||||
const MAX_GAP_LENGTH = 5;
|
const MAX_GAP_LENGTH = 5;
|
||||||
|
|
||||||
// Helper function to check if a date is a weekend
|
// Helper function to check if a date is a weekend
|
||||||
const isWeekend = date => date.getDay() === 0 || date.getDay() === 6;
|
const isWeekend = (date: Date): boolean => date.getDay() === 0 || date.getDay() === 6;
|
||||||
|
|
||||||
// Helper function to check if two dates are the same day
|
// Helper function to check if two dates are the same day
|
||||||
const isSameDay = (date1, date2) =>
|
const isSameDay = (date1: Date, date2: Date): boolean =>
|
||||||
date1.getFullYear() === date2.getFullYear() &&
|
date1.getFullYear() === date2.getFullYear() &&
|
||||||
date1.getMonth() === date2.getMonth() &&
|
date1.getMonth() === date2.getMonth() &&
|
||||||
date1.getDate() === date2.getDate();
|
date1.getDate() === date2.getDate();
|
||||||
|
|
||||||
// Helper function to generate a unique key for a date
|
// Helper function to generate a unique key for a date
|
||||||
const dateKey = date => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
const dateKey = (date: Date): string => `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
|
||||||
|
|
||||||
// Helper function to check if a date is a holiday
|
// Helper function to check if a date is a holiday
|
||||||
const isHoliday = (date, holidays) => holidays.some(h => isSameDay(h.date, date));
|
const isHoliday = (date: Date, holidays: { date: Date }[]): boolean => holidays.some(h => isSameDay(h.date, date));
|
||||||
|
|
||||||
// Helper function to check if a date is a day off
|
// Helper function to check if a date is a day off
|
||||||
const isDayOff = (date, allDaysOffSet) => allDaysOffSet.has(dateKey(date));
|
const isDayOff = (date: Date, allDaysOffSet: Set<string>): boolean => allDaysOffSet.has(dateKey(date));
|
||||||
|
|
||||||
// Helper function to calculate the number of days between two dates
|
// Helper function to calculate the number of days between two dates
|
||||||
const daysBetween = (startDate, endDate) => Math.round((endDate - startDate) / MS_IN_A_DAY);
|
const daysBetween = (startDate: Date, endDate: Date): number => Math.round((endDate.getTime() - startDate.getTime()) / MS_IN_A_DAY);
|
||||||
|
|
||||||
// Helper function to format a date
|
// Helper function to format a date
|
||||||
const formatDate = date => date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
const formatDate = (date: Date): string => date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||||
|
|
||||||
// Get holidays for a specific year and country
|
// Get holidays for a specific year and country
|
||||||
export function getHolidaysForYear(countryCode, year, stateCode = '') {
|
export function getHolidaysForYear(countryCode: string, year: number, stateCode?: string): { date: Date; name: string }[] {
|
||||||
const hd = new Holidays(countryCode, stateCode);
|
const hd = stateCode ? new Holidays(countryCode, stateCode) : new Holidays(countryCode);
|
||||||
return hd.getHolidays(year)
|
return hd.getHolidays(year)
|
||||||
.filter(holiday => holiday.type === 'public')
|
.filter(holiday => holiday.type === 'public')
|
||||||
.map(holiday => ({
|
.map(holiday => ({
|
||||||
@@ -40,14 +40,9 @@ export function getHolidaysForYear(countryCode, year, stateCode = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Optimize days off to create the longest possible chains
|
// Optimize days off to create the longest possible chains
|
||||||
export function optimizeDaysOff(holidays, year, daysOff) {
|
export function optimizeDaysOff(holidays: { date: Date }[], year: number, daysOff: number): Date[] {
|
||||||
// Filter holidays to include only those in the current year
|
|
||||||
const currentYearHolidays = holidays.filter(h => h.date.getFullYear() === year);
|
const currentYearHolidays = holidays.filter(h => h.date.getFullYear() === year);
|
||||||
|
|
||||||
// Recalculate weekends for the current year
|
|
||||||
const weekends = getWeekends(year);
|
const weekends = getWeekends(year);
|
||||||
|
|
||||||
// Initialize a new Set for all days off
|
|
||||||
const allDaysOffSet = new Set([
|
const allDaysOffSet = new Set([
|
||||||
...currentYearHolidays.map(h => dateKey(h.date)),
|
...currentYearHolidays.map(h => dateKey(h.date)),
|
||||||
...weekends.map(d => dateKey(d))
|
...weekends.map(d => dateKey(d))
|
||||||
@@ -59,15 +54,15 @@ export function optimizeDaysOff(holidays, year, daysOff) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate consecutive days off
|
// Calculate consecutive days off
|
||||||
export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) {
|
export function calculateConsecutiveDaysOff(holidays: { date: Date }[], optimizedDaysOff: Date[], year: number): { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[] {
|
||||||
const allDaysOffSet = new Set([...holidays.map(h => dateKey(h.date)), ...optimizedDaysOff.map(d => dateKey(d))]);
|
const allDaysOffSet = new Set([...holidays.map(h => dateKey(h.date)), ...optimizedDaysOff.map(d => dateKey(d))]);
|
||||||
const consecutiveDaysOff = [];
|
const consecutiveDaysOff: { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[] = [];
|
||||||
let currentGroup = [];
|
let currentGroup: Date[] = [];
|
||||||
|
|
||||||
for (let month = 0; month < 12; month++) {
|
for (let month = 0; month < 12; month++) {
|
||||||
for (let day = 1; day <= 31; day++) {
|
for (let day = 1; day <= 31; day++) {
|
||||||
const date = new Date(year, month, day);
|
const date = new Date(year, month, day);
|
||||||
if (date.getMonth() !== month) break; // Skip invalid dates
|
if (date.getMonth() !== month) break;
|
||||||
|
|
||||||
if (isWeekend(date) || isHoliday(date, holidays) || isDayOff(date, allDaysOffSet)) {
|
if (isWeekend(date) || isHoliday(date, holidays) || isDayOff(date, allDaysOffSet)) {
|
||||||
currentGroup.push(date);
|
currentGroup.push(date);
|
||||||
@@ -80,7 +75,6 @@ export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the last group at the end of the year
|
|
||||||
if (currentGroup.length > 2) {
|
if (currentGroup.length > 2) {
|
||||||
addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff);
|
addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff);
|
||||||
}
|
}
|
||||||
@@ -89,8 +83,8 @@ export function calculateConsecutiveDaysOff(holidays, optimizedDaysOff, year) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all weekends for a specific year
|
// Get all weekends for a specific year
|
||||||
function getWeekends(year) {
|
function getWeekends(year: number): Date[] {
|
||||||
const weekends = [];
|
const weekends: Date[] = [];
|
||||||
for (let month = 0; month < 12; month++) {
|
for (let month = 0; month < 12; month++) {
|
||||||
for (let day = 1; day <= 31; day++) {
|
for (let day = 1; day <= 31; day++) {
|
||||||
const date = new Date(year, month, day);
|
const date = new Date(year, month, day);
|
||||||
@@ -102,9 +96,9 @@ function getWeekends(year) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find gaps between days off
|
// Find gaps between days off
|
||||||
function findGaps(allDaysOffSet, year) {
|
function findGaps(allDaysOffSet: Set<string>, year: number): { start: Date; end: Date; gapLength: number }[] {
|
||||||
const gaps = [];
|
const gaps: { start: Date; end: Date; gapLength: number }[] = [];
|
||||||
let currentGapStart = null;
|
let currentGapStart: Date | null = null;
|
||||||
|
|
||||||
for (let month = 0; month < 12; month++) {
|
for (let month = 0; month < 12; month++) {
|
||||||
for (let day = 1; day <= 31; day++) {
|
for (let day = 1; day <= 31; day++) {
|
||||||
@@ -137,7 +131,7 @@ function findGaps(allDaysOffSet, year) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Rank gaps by efficiency
|
// Rank gaps by efficiency
|
||||||
function rankGapsByEfficiency(gaps, allDaysOffSet) {
|
function rankGapsByEfficiency(gaps: { start: Date; end: Date; gapLength: number }[], allDaysOffSet: Set<string>): any[] {
|
||||||
return gaps.map(gap => {
|
return gaps.map(gap => {
|
||||||
const backward = calculateChain(gap.start, gap.gapLength, allDaysOffSet, 'backward');
|
const backward = calculateChain(gap.start, gap.gapLength, allDaysOffSet, 'backward');
|
||||||
const forward = calculateChain(gap.end, gap.gapLength, allDaysOffSet, 'forward');
|
const forward = calculateChain(gap.end, gap.gapLength, allDaysOffSet, 'forward');
|
||||||
@@ -149,7 +143,7 @@ function rankGapsByEfficiency(gaps, allDaysOffSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate potential chain length and days off used
|
// Calculate potential chain length and days off used
|
||||||
function calculateChain(startDate, gapLength, allDaysOffSet, direction) {
|
function calculateChain(startDate: Date, gapLength: number, allDaysOffSet: Set<string>, direction: 'backward' | 'forward'): { chainLength: number; usedDaysOff: number } {
|
||||||
let chainLength = gapLength;
|
let chainLength = gapLength;
|
||||||
let usedDaysOff = 0;
|
let usedDaysOff = 0;
|
||||||
let currentDate = new Date(startDate);
|
let currentDate = new Date(startDate);
|
||||||
@@ -174,13 +168,12 @@ function calculateChain(startDate, gapLength, allDaysOffSet, direction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Select days off based on ranked gaps
|
// Select days off based on ranked gaps
|
||||||
function selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year) {
|
function selectDaysOff(rankedGaps: any[], daysOff: number, allDaysOffSet: Set<string>, year: number): Date[] {
|
||||||
const selectedDays = [];
|
const selectedDays: Date[] = [];
|
||||||
|
|
||||||
while (daysOff > 0 && rankedGaps.length > 0) {
|
while (daysOff > 0 && rankedGaps.length > 0) {
|
||||||
const gap = rankedGaps.shift(); // Get the highest-ranked gap
|
const gap = rankedGaps.shift();
|
||||||
|
|
||||||
// Determine the direction and starting point for filling the gap
|
|
||||||
const increment = gap.fillFrom === 'start' ? 1 : -1;
|
const increment = gap.fillFrom === 'start' ? 1 : -1;
|
||||||
const startDate = gap.fillFrom === 'start' ? gap.start : gap.end;
|
const startDate = gap.fillFrom === 'start' ? gap.start : gap.end;
|
||||||
|
|
||||||
@@ -195,7 +188,6 @@ function selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate gaps and re-rank them after each assignment
|
|
||||||
const newGaps = findGaps(allDaysOffSet, year);
|
const newGaps = findGaps(allDaysOffSet, year);
|
||||||
rankedGaps = rankGapsByEfficiency(newGaps, allDaysOffSet);
|
rankedGaps = rankGapsByEfficiency(newGaps, allDaysOffSet);
|
||||||
}
|
}
|
||||||
@@ -204,7 +196,7 @@ function selectDaysOff(rankedGaps, daysOff, allDaysOffSet, year) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add consecutive days off to the list
|
// Add consecutive days off to the list
|
||||||
function addConsecutiveDaysOff(consecutiveDaysOff, currentGroup, optimizedDaysOff) {
|
function addConsecutiveDaysOff(consecutiveDaysOff: { startDate: Date; endDate: Date; usedDaysOff: number; totalDays: number }[], currentGroup: Date[], optimizedDaysOff: Date[]) {
|
||||||
const startDate = currentGroup[0];
|
const startDate = currentGroup[0];
|
||||||
const endDate = currentGroup[currentGroup.length - 1];
|
const endDate = currentGroup[currentGroup.length - 1];
|
||||||
const totalDays = daysBetween(startDate, endDate) + 1;
|
const totalDays = daysBetween(startDate, endDate) + 1;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export const ptoData = {
|
export const ptoData: Record<string, number> = {
|
||||||
// Afghanistan
|
// Afghanistan
|
||||||
AF: 20, // 20 days recreational leave, 15 paid public holidays
|
AF: 20, // 20 days recreational leave, 15 paid public holidays
|
||||||
|
|
||||||
@@ -1,62 +1,62 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { injectSpeedInsights } from '@vercel/speed-insights';
|
import { injectSpeedInsights } from '@vercel/speed-insights';
|
||||||
import { inject } from '@vercel/analytics'
|
import { inject } from '@vercel/analytics';
|
||||||
import countries from 'i18n-iso-countries';
|
import countries from 'i18n-iso-countries';
|
||||||
import enLocale from 'i18n-iso-countries/langs/en.json';
|
import enLocale from 'i18n-iso-countries/langs/en.json';
|
||||||
import CalendarMonth from '../lib/CalendarMonth.svelte';
|
import CalendarMonth from '../lib/CalendarMonth.svelte';
|
||||||
import { getHolidaysForYear, optimizeDaysOff, calculateConsecutiveDaysOff } from '../lib/holidayUtils.js';
|
import { getHolidaysForYear, optimizeDaysOff, calculateConsecutiveDaysOff } from '../lib/holidayUtils';
|
||||||
import { ptoData } from '../lib/ptoData.js';
|
import { ptoData } from '../lib/ptoData';
|
||||||
import Holidays from 'date-holidays';
|
import Holidays from 'date-holidays';
|
||||||
|
|
||||||
countries.registerLocale(enLocale);
|
countries.registerLocale(enLocale);
|
||||||
let countriesList = countries.getNames('en');
|
let countriesList: Record<string, string> = countries.getNames('en');
|
||||||
|
|
||||||
let year;
|
let year: number;
|
||||||
let months = Array.from({ length: 12 }, (_, i) => i);
|
let months: number[] = Array.from({ length: 12 }, (_, i) => i);
|
||||||
let selectedCountry = '';
|
let selectedCountry: string = '';
|
||||||
let holidays = [];
|
let holidays: Array<{ date: Date; name: string; hidden?: boolean }> = [];
|
||||||
let daysOff = 0;
|
let daysOff: number = 0;
|
||||||
let optimizedDaysOff = [];
|
let optimizedDaysOff: Date[] = [];
|
||||||
let consecutiveDaysOff = [];
|
let consecutiveDaysOff: Array<{ startDate: Date; endDate: Date; totalDays: number }> = [];
|
||||||
let placeholder = "Country";
|
let countriesInput: HTMLInputElement | null = null;
|
||||||
let countriesInput;
|
let statesInput: HTMLInputElement | null = null;
|
||||||
let statesInput;
|
let showHowItWorks: boolean = false;
|
||||||
let showHowItWorks = false;
|
|
||||||
|
|
||||||
// Default settings
|
// Default settings
|
||||||
let defaultYear = new Date().getFullYear();
|
let defaultYear: number = new Date().getFullYear();
|
||||||
let defaultCountry = '';
|
let defaultCountry: string = '';
|
||||||
let defaultDaysOff = 0;
|
let defaultDaysOff: number = 0;
|
||||||
|
|
||||||
let selectedState = '';
|
let selectedState: string = '';
|
||||||
let selectedStateCode = '';
|
let selectedStateCode: string = '';
|
||||||
let statesList = [];
|
let statesList: Record<string, string> = {};
|
||||||
|
|
||||||
let showHolidaysList = false; // State to toggle the visibility of the holidays list
|
let showHolidaysList: boolean = false;
|
||||||
|
|
||||||
$: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry);
|
$: selectedCountryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry) || '';
|
||||||
|
|
||||||
$: if (selectedCountryCode || selectedStateCode || daysOff || year) {
|
$: if (selectedCountryCode || selectedStateCode || daysOff || year) {
|
||||||
updateHolidays();
|
updateHolidays();
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (daysOff) {
|
$: if (daysOff) {
|
||||||
localStorage.setItem('daysOff', daysOff);
|
localStorage.setItem('daysOff', daysOff.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (year) {
|
$: if (year) {
|
||||||
localStorage.setItem('year', year);
|
localStorage.setItem('year', year.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateStatesList(countryCode) {
|
function updateStatesList(countryCode: string) {
|
||||||
const hd = new Holidays(countryCode);
|
const hd = new Holidays(countryCode);
|
||||||
statesList = hd.getStates(countryCode) || [];
|
statesList = hd.getStates(countryCode) || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStateChange(event) {
|
function handleStateChange(event: Event) {
|
||||||
const stateName = event.target.value;
|
const target = event.target as HTMLInputElement;
|
||||||
selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName);
|
const stateName = target.value;
|
||||||
|
selectedStateCode = Object.keys(statesList).find(code => statesList[code] === stateName) || '';
|
||||||
selectedState = stateName;
|
selectedState = stateName;
|
||||||
localStorage.setItem('selectedState', selectedState);
|
localStorage.setItem('selectedState', selectedState);
|
||||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||||
@@ -66,11 +66,10 @@
|
|||||||
inject();
|
inject();
|
||||||
injectSpeedInsights();
|
injectSpeedInsights();
|
||||||
|
|
||||||
// Always fetch the real country and PTO data
|
|
||||||
fetchCountryCode().then(() => {
|
fetchCountryCode().then(() => {
|
||||||
defaultYear = new Date().getFullYear();
|
defaultYear = new Date().getFullYear();
|
||||||
defaultCountry = selectedCountry;
|
defaultCountry = selectedCountry;
|
||||||
defaultDaysOff = ptoData[selectedCountryCode];
|
defaultDaysOff = ptoData[selectedCountryCode] || 0;
|
||||||
|
|
||||||
const storedYear = localStorage.getItem('year');
|
const storedYear = localStorage.getItem('year');
|
||||||
const storedCountry = localStorage.getItem('selectedCountry');
|
const storedCountry = localStorage.getItem('selectedCountry');
|
||||||
@@ -104,8 +103,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCountryChange(event) {
|
function handleCountryChange(event: Event) {
|
||||||
const fullValue = event.target.value;
|
const target = event.target as HTMLInputElement;
|
||||||
|
const fullValue = target.value;
|
||||||
if (selectedCountryCode) {
|
if (selectedCountryCode) {
|
||||||
daysOff = ptoData[selectedCountryCode] || 0;
|
daysOff = ptoData[selectedCountryCode] || 0;
|
||||||
selectedState = ''; // Reset state
|
selectedState = ''; // Reset state
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
localStorage.setItem('selectedCountry', selectedCountry);
|
localStorage.setItem('selectedCountry', selectedCountry);
|
||||||
localStorage.setItem('selectedState', selectedState);
|
localStorage.setItem('selectedState', selectedState);
|
||||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||||
localStorage.setItem('daysOff', daysOff);
|
localStorage.setItem('daysOff', daysOff.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +124,11 @@
|
|||||||
let allHolidays = getHolidaysForYear(selectedCountryCode, year, selectedStateCode);
|
let allHolidays = getHolidaysForYear(selectedCountryCode, year, selectedStateCode);
|
||||||
holidays = allHolidays.map(holiday => ({
|
holidays = allHolidays.map(holiday => ({
|
||||||
...holiday,
|
...holiday,
|
||||||
|
date: new Date(holiday.date),
|
||||||
hidden: isHolidayHidden(holiday)
|
hidden: isHolidayHidden(holiday)
|
||||||
}));
|
}));
|
||||||
const visibleHolidays = holidays.filter(h => !h.hidden);
|
const visibleHolidays = holidays
|
||||||
|
.filter(h => !h.hidden);
|
||||||
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff);
|
optimizedDaysOff = optimizeDaysOff(visibleHolidays, year, daysOff);
|
||||||
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year);
|
consecutiveDaysOff = calculateConsecutiveDaysOff(visibleHolidays, optimizedDaysOff, year);
|
||||||
} else {
|
} else {
|
||||||
@@ -142,14 +144,14 @@
|
|||||||
selectedState = '';
|
selectedState = '';
|
||||||
selectedStateCode = '';
|
selectedStateCode = '';
|
||||||
daysOff = defaultDaysOff;
|
daysOff = defaultDaysOff;
|
||||||
localStorage.setItem('year', year);
|
localStorage.setItem('year', year.toString());
|
||||||
localStorage.setItem('selectedCountry', selectedCountry);
|
localStorage.setItem('selectedCountry', selectedCountry);
|
||||||
localStorage.setItem('selectedState', selectedState);
|
localStorage.setItem('selectedState', selectedState);
|
||||||
localStorage.setItem('selectedStateCode', selectedStateCode);
|
localStorage.setItem('selectedStateCode', selectedStateCode);
|
||||||
localStorage.setItem('daysOff', daysOff);
|
localStorage.setItem('daysOff', daysOff.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -176,8 +178,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function adjustInputWidth(inputElement, value) {
|
function adjustInputWidth(inputElement: HTMLInputElement | null, value: string) {
|
||||||
if (typeof window !== 'undefined' && inputElement) { // Ensure this runs only in the browser
|
if (typeof window !== 'undefined' && inputElement) {
|
||||||
const tempSpan = document.createElement('span');
|
const tempSpan = document.createElement('span');
|
||||||
tempSpan.style.visibility = 'hidden';
|
tempSpan.style.visibility = 'hidden';
|
||||||
tempSpan.style.position = 'absolute';
|
tempSpan.style.position = 'absolute';
|
||||||
@@ -189,15 +191,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactive statements to adjust input width when values change
|
|
||||||
$: if (countriesInput) adjustInputWidth(countriesInput, selectedCountry);
|
$: if (countriesInput) adjustInputWidth(countriesInput, selectedCountry);
|
||||||
$: if (statesInput) adjustInputWidth(statesInput, selectedState);
|
$: if (statesInput) adjustInputWidth(statesInput, selectedState);
|
||||||
|
|
||||||
function getFlagEmoji(countryCode) {
|
function getFlagEmoji(countryCode: string) {
|
||||||
if (!countryCode) return ''; // Return an empty string if countryCode is not available
|
if (!countryCode) return '';
|
||||||
return countryCode
|
return countryCode
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt()));
|
.replace(/./g, char => String.fromCodePoint(127397 + char.charCodeAt(0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleHowItWorks() {
|
function toggleHowItWorks() {
|
||||||
@@ -210,48 +211,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to toggle the visibility of a holiday
|
function toggleHolidayVisibility(holiday: { date: Date; name: string; hidden?: boolean }) {
|
||||||
function toggleHolidayVisibility(holiday) {
|
if (!selectedCountryCode) return;
|
||||||
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry);
|
|
||||||
if (!countryCode) return;
|
|
||||||
|
|
||||||
const storageKey = `hiddenHolidays_${countryCode}`;
|
const storageKey = `hiddenHolidays_${selectedCountryCode}`;
|
||||||
let hiddenHolidays = JSON.parse(localStorage.getItem(storageKey)) || {};
|
let hiddenHolidays = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
||||||
|
|
||||||
// Toggle the hidden state of the holiday
|
hiddenHolidays[holiday.date.toString()] = !hiddenHolidays[holiday.date.toString()];
|
||||||
hiddenHolidays[holiday.date] = !hiddenHolidays[holiday.date];
|
|
||||||
localStorage.setItem(storageKey, JSON.stringify(hiddenHolidays));
|
localStorage.setItem(storageKey, JSON.stringify(hiddenHolidays));
|
||||||
|
|
||||||
// Update the holidays list to trigger reactivity
|
|
||||||
holidays = holidays.map(h => {
|
holidays = holidays.map(h => {
|
||||||
if (h.date === holiday.date) {
|
if (h.date === holiday.date) {
|
||||||
return { ...h, hidden: hiddenHolidays[h.date] };
|
return { ...h, hidden: hiddenHolidays[h.date.toString()] };
|
||||||
}
|
}
|
||||||
return h;
|
return h;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recalculate holidays to ensure hidden ones are excluded
|
|
||||||
updateHolidays();
|
updateHolidays();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to check if a holiday is hidden
|
function isHolidayHidden(holiday: { date: Date; name: string; hidden?: boolean }) {
|
||||||
function isHolidayHidden(holiday) {
|
if (!selectedCountryCode) return false;
|
||||||
const countryCode = Object.keys(countriesList).find(code => countriesList[code] === selectedCountry);
|
|
||||||
if (!countryCode) return false;
|
|
||||||
|
|
||||||
const storageKey = `hiddenHolidays_${countryCode}`;
|
const storageKey = `hiddenHolidays_${selectedCountryCode}`;
|
||||||
const hiddenHolidays = JSON.parse(localStorage.getItem(storageKey)) || {};
|
const hiddenHolidays = JSON.parse(localStorage.getItem(storageKey) || '{}');
|
||||||
|
|
||||||
return hiddenHolidays[holiday.date] || false;
|
return hiddenHolidays[holiday.date.toString()] || false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to format the date
|
function formatDate(date: Date) {
|
||||||
function formatDate(dateString) {
|
const options: Intl.DateTimeFormatOptions = { day: '2-digit', month: 'short', year: 'numeric' };
|
||||||
const options = { day: '2-digit', month: 'short', year: 'numeric' };
|
return new Date(date).toLocaleDateString('en-GB', options);
|
||||||
return new Date(dateString).toLocaleDateString('en-GB', options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactive statement to calculate the number of visible holidays
|
|
||||||
$: visibleHolidaysCount = holidays.filter(h => !h.hidden).length;
|
$: visibleHolidaysCount = holidays.filter(h => !h.hidden).length;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -321,7 +313,7 @@
|
|||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.calendar-grid {
|
.calendar-grid {
|
||||||
grid-template-columns: repeat(2, 1fr); /* Allow 2 columns on smaller screens */
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
@@ -400,23 +392,23 @@
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
border-left: 4px solid #111; /* Lighter border on the left */
|
border-left: 4px solid #111;
|
||||||
border-top: 4px solid #111; /* Lighter border on the top */
|
border-top: 4px solid #111;
|
||||||
border-right: 4px solid #555; /* Darker border on the right */
|
border-right: 4px solid #555;
|
||||||
border-bottom: 4px solid #555; /* Darker border on the bottom */
|
border-bottom: 4px solid #555;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 0.8em; /* Smaller font size */
|
font-size: 0.8em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 3px; /* Reduced padding */
|
padding: 3px;
|
||||||
margin: 0 3px; /* Reduced margin */
|
margin: 0 3px;
|
||||||
border-radius: 4px; /* Slightly less rounded edges */
|
border-radius: 4px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
transition: transform 0.1s;
|
transition: transform 0.1s;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 30px; /* Smaller width */
|
width: 30px;
|
||||||
height: 30px; /* Smaller height */
|
height: 30px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,18 +435,6 @@
|
|||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day {
|
|
||||||
aspect-ratio: 1;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.6em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #ddd;
|
|
||||||
background-color: #222;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.how-it-works {
|
.how-it-works {
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
padding: 25px;
|
padding: 25px;
|
||||||
@@ -482,10 +462,22 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
transition: color 0.3s;
|
transition: color 0.3s;
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-text:hover {
|
.toggle-text:hover {
|
||||||
color: #61dafb;
|
color: #61dafb;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-text:focus {
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-link {
|
.reset-link {
|
||||||
@@ -579,7 +571,7 @@
|
|||||||
<p>
|
<p>
|
||||||
In
|
In
|
||||||
<strong>
|
<strong>
|
||||||
{getFlagEmoji(Object.keys(countriesList).find(code => countriesList[code] === selectedCountry))}
|
{getFlagEmoji(selectedCountryCode)}
|
||||||
{#if selectedState}
|
{#if selectedState}
|
||||||
{selectedState},
|
{selectedState},
|
||||||
{/if}
|
{/if}
|
||||||
@@ -594,9 +586,13 @@
|
|||||||
<div class="content-box">
|
<div class="content-box">
|
||||||
<p>
|
<p>
|
||||||
I live in
|
I live in
|
||||||
<span class="flag" style="vertical-align: middle;">{getFlagEmoji(Object.keys(countriesList).find(code => countriesList[code] === selectedCountry))}</span>
|
<span class="flag" style="vertical-align: middle;">{getFlagEmoji(selectedCountryCode)}</span>
|
||||||
{#if Object.keys(statesList).length > 0}
|
{#if Object.keys(statesList).length > 0}
|
||||||
<input bind:this={statesInput} list="states" class="editable-input bold" bind:value={selectedState} on:input={(e) => { handleStateChange(e); }} on:focus={() => { statesInput.value = ''; }} placeholder="State" aria-label="State" />
|
<input bind:this={statesInput} list="states" class="editable-input bold" bind:value={selectedState}
|
||||||
|
on:input={(e) => handleStateChange(e)}
|
||||||
|
on:focus={(e) => { (e.target as HTMLInputElement).value = ''; }}
|
||||||
|
placeholder="State"
|
||||||
|
aria-label="State" />
|
||||||
<datalist id="states">
|
<datalist id="states">
|
||||||
{#each Object.entries(statesList) as [code, name]}
|
{#each Object.entries(statesList) as [code, name]}
|
||||||
<option value={name}>{name}</option>
|
<option value={name}>{name}</option>
|
||||||
@@ -604,7 +600,11 @@
|
|||||||
</datalist>
|
</datalist>
|
||||||
in
|
in
|
||||||
{/if}
|
{/if}
|
||||||
<input bind:this={countriesInput} list="countries" class="editable-input bold" bind:value={selectedCountry} placeholder={placeholder} on:input={(e) => { handleCountryChange(e); }} on:focus={() => { countriesInput.value = ''; }} aria-label="Select country" />
|
<input bind:this={countriesInput} list="countries" class="editable-input bold" bind:value={selectedCountry}
|
||||||
|
on:input={(e) => handleCountryChange(e)}
|
||||||
|
on:focus={(e) => { (e.target as HTMLInputElement).value = ''; }}
|
||||||
|
placeholder="Country"
|
||||||
|
aria-label="Select country" />
|
||||||
and have
|
and have
|
||||||
<span class="arrow-controls">
|
<span class="arrow-controls">
|
||||||
<button on:click={() => { if (daysOff > 0) { daysOff--; updateHolidays(); } }} aria-label="Decrease days off">▼</button>
|
<button on:click={() => { if (daysOff > 0) { daysOff--; updateHolidays(); } }} aria-label="Decrease days off">▼</button>
|
||||||
@@ -682,15 +682,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toggle-text" on:click={toggleHowItWorks}>
|
<button type="button" class="toggle-text" on:click={toggleHowItWorks}>
|
||||||
{showHowItWorks ? 'Hide Details' : 'How does this work?'}
|
{showHowItWorks ? 'Hide Details' : 'How does this work?'}
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{#if showHowItWorks}
|
{#if showHowItWorks}
|
||||||
<div id="how-it-works" class="content-box how-it-works">
|
<div id="how-it-works" class="content-box how-it-works">
|
||||||
<h3>How does this work?</h3>
|
<h3>How does this work?</h3>
|
||||||
<p>
|
<p>
|
||||||
This tool detects your country from your IP, uses a default number of government-mandated days off from <a href="https://en.wikipedia.org/wiki/List_of_minimum_annual_leave_by_country" target="_blank" rel="noopener noreferrer">Wikipedia</a>, and a <a href={`https://github.com/commenthol/date-holidays/blob/master/data/countries/${Object.keys(countriesList).find(code => countriesList[code] === selectedCountry)}.yaml`} target="_blank" rel="noopener noreferrer">list of holidays</a> for {selectedCountry}.
|
This tool detects your country from your IP, uses a default number of government-mandated days off from <a href="https://en.wikipedia.org/wiki/List_of_minimum_annual_leave_by_country" target="_blank" rel="noopener noreferrer">Wikipedia</a>, and a <a href={`https://github.com/commenthol/date-holidays/blob/master/data/countries/${selectedCountryCode}.yaml`} target="_blank" rel="noopener noreferrer">list of holidays</a> for {selectedCountry}.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
The algorithm prioritizes filling the shortest gaps first. It optimizes for spreading your holidays throughout the year to create the most number of consecutive vacation periods.
|
The algorithm prioritizes filling the shortest gaps first. It optimizes for spreading your holidays throughout the year to create the most number of consecutive vacation periods.
|
||||||
|
|||||||
Reference in New Issue
Block a user