feat: multiple holiday calendars per vacay plan

- Add vacay_holiday_calendars table (region, label, color, sort_order)
- Lazy migration of existing holidays_region to first calendar row
- Extract applyHolidayCalendars() helper; replace inline holiday logic
- GET /vacay/plan now includes holiday_calendars array
- Add POST/PUT/DELETE /vacay/plan/holiday-calendars/:id endpoints
- Client VacayPlan/VacayEntry/HolidayInfo types updated
- loadHolidays() loops over all calendars; per-calendar color on HolidayInfo
- VacayMonthCard uses holiday.color instead of hardcoded red
- VacaySettings replaced single country picker with calendar list UI
- VacayPage legend renders one item per calendar
- i18n: addCalendar, calendarLabel, calendarColor, noCalendars (en + de)
- Fix pre-existing TS errors: VacayPlan/VacayEntry missing fields,
  SettingToggleProps icon/onChange types, packing.suggestions.items array type

Closes #36
This commit is contained in:
Stephen Wheet
2026-03-28 22:16:12 +00:00
parent 1f9ae8e4b5
commit 3c4f5f7193
10 changed files with 421 additions and 122 deletions

View File

@@ -3,7 +3,7 @@ import { useSettingsStore } from '../store/settingsStore'
import de from './translations/de'
import en from './translations/en'
type TranslationStrings = Record<string, string>
type TranslationStrings = Record<string, string | { name: string; category: string }[]>
const translations: Record<string, TranslationStrings> = { de, en }
@@ -27,7 +27,7 @@ export function TranslationProvider({ children }: TranslationProviderProps) {
const fallback = translations.de
function t(key: string, params?: Record<string, string | number>): string {
let val: string = strings[key] ?? fallback[key] ?? key
let val: string = (strings[key] ?? fallback[key] ?? key) as string
if (params) {
Object.entries(params).forEach(([k, v]) => {
val = val.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v))

View File

@@ -1,4 +1,4 @@
const de: Record<string, string> = {
const de: Record<string, string | { name: string; category: string }[]> = {
// Allgemein
'common.save': 'Speichern',
'common.cancel': 'Abbrechen',
@@ -390,6 +390,10 @@ const de: Record<string, string> = {
'vacay.publicHolidaysHint': 'Feiertage im Kalender markieren',
'vacay.selectCountry': 'Land wählen',
'vacay.selectRegion': 'Region wählen (optional)',
'vacay.addCalendar': 'Kalender hinzufügen',
'vacay.calendarLabel': 'Bezeichnung (optional)',
'vacay.calendarColor': 'Farbe',
'vacay.noCalendars': 'Noch keine Feiertagskalender angelegt',
'vacay.companyHolidays': 'Betriebsferien',
'vacay.companyHolidaysHint': 'Erlaubt das Markieren von unternehmensweiten Feiertagen',
'vacay.companyHolidaysNoDeduct': 'Betriebsferien werden nicht vom Urlaubskontingent abgezogen.',

View File

@@ -1,4 +1,4 @@
const en: Record<string, string> = {
const en: Record<string, string | { name: string; category: string }[]> = {
// Common
'common.save': 'Save',
'common.cancel': 'Cancel',
@@ -390,6 +390,10 @@ const en: Record<string, string> = {
'vacay.publicHolidaysHint': 'Mark public holidays in the calendar',
'vacay.selectCountry': 'Select country',
'vacay.selectRegion': 'Select region (optional)',
'vacay.addCalendar': 'Add calendar',
'vacay.calendarLabel': 'Label (optional)',
'vacay.calendarColor': 'Color',
'vacay.noCalendars': 'No holiday calendars added yet',
'vacay.companyHolidays': 'Company Holidays',
'vacay.companyHolidaysHint': 'Allow marking company-wide holiday days',
'vacay.companyHolidaysNoDeduct': 'Company holidays do not count towards vacation days.',