From f323952012fd54ac10cf0175df804c426e0fc762 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 12 Apr 2026 02:18:45 +0200 Subject: [PATCH] feat: configurable week start day in Vacay (Monday or Sunday) - New setting in Vacay Settings to choose Mon or Sun as week start - DB migration adds week_start column to vacay_plans (default: Monday) - Calendar grid and weekday headers adapt to the selected start day - Weekend column highlighting works correctly for both modes - Translations added for all 14 languages --- client/src/components/Vacay/VacayCalendar.tsx | 1 + .../src/components/Vacay/VacayMonthCard.tsx | 28 ++++++++++------ client/src/components/Vacay/VacaySettings.tsx | 33 ++++++++++++++++++- client/src/i18n/translations/ar.ts | 2 ++ client/src/i18n/translations/br.ts | 2 ++ client/src/i18n/translations/cs.ts | 2 ++ client/src/i18n/translations/de.ts | 2 ++ client/src/i18n/translations/en.ts | 2 ++ client/src/i18n/translations/es.ts | 2 ++ client/src/i18n/translations/fr.ts | 2 ++ client/src/i18n/translations/hu.ts | 2 ++ client/src/i18n/translations/it.ts | 2 ++ client/src/i18n/translations/nl.ts | 2 ++ client/src/i18n/translations/pl.ts | 2 ++ client/src/i18n/translations/ru.ts | 2 ++ client/src/i18n/translations/zh.ts | 2 ++ client/src/i18n/translations/zhTw.ts | 2 ++ client/src/types.ts | 1 + server/src/db/migrations.ts | 4 +++ server/src/services/vacayService.ts | 4 ++- 20 files changed, 87 insertions(+), 12 deletions(-) diff --git a/client/src/components/Vacay/VacayCalendar.tsx b/client/src/components/Vacay/VacayCalendar.tsx index f016c72d..0dcf7511 100644 --- a/client/src/components/Vacay/VacayCalendar.tsx +++ b/client/src/components/Vacay/VacayCalendar.tsx @@ -85,6 +85,7 @@ export default function VacayCalendar() { blockWeekends={blockWeekends} weekendDays={weekendDays} tripDates={tripDates} + weekStart={plan?.week_start ?? 1} /> ))} diff --git a/client/src/components/Vacay/VacayMonthCard.tsx b/client/src/components/Vacay/VacayMonthCard.tsx index 1e3bd78b..29aebe95 100644 --- a/client/src/components/Vacay/VacayMonthCard.tsx +++ b/client/src/components/Vacay/VacayMonthCard.tsx @@ -24,22 +24,25 @@ interface VacayMonthCardProps { blockWeekends: boolean weekendDays?: number[] tripDates?: Set + weekStart?: number } export default function VacayMonthCard({ year, month, holidays, companyHolidaySet, companyHolidaysEnabled = true, entryMap, - onCellClick, companyMode, blockWeekends, weekendDays = [0, 6], tripDates + onCellClick, companyMode, blockWeekends, weekendDays = [0, 6], tripDates, weekStart = 1 }: VacayMonthCardProps) { const { t, locale } = useTranslation() - const weekdays = WEEKDAY_KEYS.map(k => t(k)) + const WEEKDAY_KEYS_SUNDAY = ['vacay.sun', 'vacay.mon', 'vacay.tue', 'vacay.wed', 'vacay.thu', 'vacay.fri', 'vacay.sat'] as const + const orderedKeys = weekStart === 0 ? WEEKDAY_KEYS_SUNDAY : WEEKDAY_KEYS + const weekdays = orderedKeys.map(k => t(k)) const monthName = useMemo(() => new Intl.DateTimeFormat(locale, { month: 'long' }).format(new Date(year, month, 1)), [locale, year, month]) - + const weeks = useMemo(() => { const firstDay = new Date(year, month, 1) const daysInMonth = new Date(year, month + 1, 0).getDate() - let startDow = firstDay.getDay() - 1 - if (startDow < 0) startDow = 6 + let startDow = firstDay.getDay() - weekStart + if (startDow < 0) startDow += 7 const cells = [] for (let i = 0; i < startDow; i++) cells.push(null) for (let d = 1; d <= daysInMonth; d++) cells.push(d) @@ -58,11 +61,16 @@ export default function VacayMonthCard({
- {weekdays.map((wd, i) => ( -
= 5 ? 'var(--text-faint)' : 'var(--text-muted)' }}> - {wd} -
- ))} + {weekdays.map((wd, i) => { + // Map column index back to JS day (0=Sun..6=Sat) to check if it's a weekend column + const jsDay = (i + weekStart) % 7 + const isWeekendCol = weekendDays.includes(jsDay) + return ( +
+ {wd} +
+ ) + })}
diff --git a/client/src/components/Vacay/VacaySettings.tsx b/client/src/components/Vacay/VacaySettings.tsx index 36b48757..14efe606 100644 --- a/client/src/components/Vacay/VacaySettings.tsx +++ b/client/src/components/Vacay/VacaySettings.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react' -import { type LucideIcon, CalendarOff, AlertCircle, Building2, Unlink, ArrowRightLeft, Globe, Plus, Trash2 } from 'lucide-react' +import { type LucideIcon, CalendarOff, AlertCircle, Building2, Unlink, ArrowRightLeft, Globe, Plus, Trash2, CalendarDays } from 'lucide-react' import { useVacayStore } from '../../store/vacayStore' import { getIntlLanguage, useTranslation } from '../../i18n' import { useToast } from '../shared/Toast' @@ -85,6 +85,37 @@ export default function VacaySettings({ onClose }: VacaySettingsProps) {
)} + {/* Week start */} +
+
+ +
+ {t('vacay.weekStart')} +

{t('vacay.weekStartHint')}

+
+
+
+ {[ + { value: 1, label: t('vacay.mon') }, + { value: 0, label: t('vacay.sun') }, + ].map(({ value, label }) => { + const active = (plan.week_start ?? 1) === value + return ( + + ) + })} +
+
+ {/* Carry-over */} = { 'vacay.companyHolidays': 'عطل الشركة', 'vacay.companyHolidaysHint': 'السماح بوضع علامة على أيام عطلات الشركة', 'vacay.companyHolidaysNoDeduct': 'لا تُخصم عطل الشركة من أيام الإجازة.', + 'vacay.weekStart': 'يبدأ الأسبوع في', + 'vacay.weekStartHint': 'اختر ما إذا كان الأسبوع يبدأ يوم الاثنين أو الأحد', 'vacay.carryOver': 'الترحيل', 'vacay.carryOverHint': 'ترحيل أيام الإجازة المتبقية تلقائيًا إلى السنة التالية', 'vacay.sharing': 'المشاركة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 8a065251..e9675d25 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -696,6 +696,8 @@ const br: Record = { 'vacay.companyHolidays': 'Feriados da empresa', 'vacay.companyHolidaysHint': 'Permitir marcar dias de feriado em toda a empresa', 'vacay.companyHolidaysNoDeduct': 'Feriados da empresa não contam como dias de férias.', + 'vacay.weekStart': 'Semana começa em', + 'vacay.weekStartHint': 'Escolha se a semana começa na segunda-feira ou no domingo', 'vacay.carryOver': 'Acúmulo', 'vacay.carryOverHint': 'Levar automaticamente os dias de férias restantes para o ano seguinte', 'vacay.sharing': 'Compartilhamento', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index fd7b925c..2b8dbb78 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -726,6 +726,8 @@ const cs: Record = { 'vacay.companyHolidays': 'Firemní volno', 'vacay.companyHolidaysHint': 'Povolit označování dnů celofiremního volna', 'vacay.companyHolidaysNoDeduct': 'Firemní volno se nezapočítává do nároku na dovolenou.', + 'vacay.weekStart': 'Týden začíná', + 'vacay.weekStartHint': 'Zvolte, zda týden začíná v pondělí nebo v neděli', 'vacay.carryOver': 'Převod dovolené', 'vacay.carryOverHint': 'Automaticky převádět zbývající dny do dalšího roku', 'vacay.sharing': 'Sdílení', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 8219005e..2f93e9e7 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -727,6 +727,8 @@ const de: Record = { 'vacay.companyHolidays': 'Betriebsferien', 'vacay.companyHolidaysHint': 'Erlaubt das Markieren von unternehmensweiten Feiertagen', 'vacay.companyHolidaysNoDeduct': 'Betriebsferien werden nicht vom Urlaubskontingent abgezogen.', + 'vacay.weekStart': 'Woche beginnt am', + 'vacay.weekStartHint': 'Wähle ob die Kalenderwoche am Montag oder Sonntag beginnt', 'vacay.carryOver': 'Urlaubsmitnahme', 'vacay.carryOverHint': 'Resturlaub automatisch ins Folgejahr übertragen', 'vacay.sharing': 'Teilen', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index fcb940b0..da78e7c4 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -749,6 +749,8 @@ const en: Record = { 'vacay.companyHolidays': 'Company Holidays', 'vacay.companyHolidaysHint': 'Allow marking company-wide holiday days', 'vacay.companyHolidaysNoDeduct': 'Company holidays do not count towards vacation days.', + 'vacay.weekStart': 'Week starts on', + 'vacay.weekStartHint': 'Choose whether the calendar week starts on Monday or Sunday', 'vacay.carryOver': 'Carry Over', 'vacay.carryOverHint': 'Automatically carry remaining vacation days into the next year', 'vacay.sharing': 'Sharing', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 3cc64874..4200c973 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -696,6 +696,8 @@ const es: Record = { 'vacay.companyHolidays': 'Festivos de empresa', 'vacay.companyHolidaysHint': 'Permitir marcar días festivos comunes de la empresa', 'vacay.companyHolidaysNoDeduct': 'Los festivos de empresa no descuentan días de vacaciones.', + 'vacay.weekStart': 'La semana comienza el', + 'vacay.weekStartHint': 'Elige si la semana comienza el lunes o el domingo', 'vacay.carryOver': 'Arrastrar saldo', 'vacay.carryOverHint': 'Trasladar automáticamente los días restantes al año siguiente', 'vacay.sharing': 'Compartir', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index c04b5130..001034ef 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -719,6 +719,8 @@ const fr: Record = { 'vacay.companyHolidays': 'Jours fériés d\'entreprise', 'vacay.companyHolidaysHint': 'Autoriser le marquage des jours fériés d\'entreprise', 'vacay.companyHolidaysNoDeduct': 'Les jours fériés d\'entreprise ne sont pas déduits des jours de vacances.', + 'vacay.weekStart': 'La semaine commence le', + 'vacay.weekStartHint': 'Choisissez si la semaine commence le lundi ou le dimanche', 'vacay.carryOver': 'Report', 'vacay.carryOverHint': 'Reporter automatiquement les jours de vacances restants à l\'année suivante', 'vacay.sharing': 'Partage', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index c83a2323..bc92c8b7 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -724,6 +724,8 @@ const hu: Record = { 'vacay.companyHolidays': 'Céges szabadnapok', 'vacay.companyHolidaysHint': 'Céges szintű szabadnapok megjelölésének engedélyezése', 'vacay.companyHolidaysNoDeduct': 'A céges szabadnapok nem számítanak bele a szabadságkeretbe.', + 'vacay.weekStart': 'A hét kezdőnapja', + 'vacay.weekStartHint': 'Válaszd ki, hogy a hét hétfőn vagy vasárnap kezdődjön', 'vacay.carryOver': 'Szabadság átvitele', 'vacay.carryOverHint': 'Megmaradt szabadságnapok automatikus átvitele a következő évre', 'vacay.sharing': 'Megosztás', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index ddfce2e3..756a8a57 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -724,6 +724,8 @@ const it: Record = { 'vacay.companyHolidays': 'Ferie aziendali', 'vacay.companyHolidaysHint': 'Consenti di segnare giorni di ferie aziendali', 'vacay.companyHolidaysNoDeduct': 'Le ferie aziendali non vengono conteggiate nei giorni di ferie.', + 'vacay.weekStart': 'La settimana inizia il', + 'vacay.weekStartHint': 'Scegli se la settimana inizia il lunedì o la domenica', 'vacay.carryOver': 'Riporto', 'vacay.carryOverHint': 'Riporta automaticamente i giorni di ferie rimanenti all\'anno successivo', 'vacay.sharing': 'Condivisione', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index ca1c4519..4353ee60 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -719,6 +719,8 @@ const nl: Record = { 'vacay.companyHolidays': 'Bedrijfsvakanties', 'vacay.companyHolidaysHint': 'Sta het markeren van bedrijfsbrede vakantiedagen toe', 'vacay.companyHolidaysNoDeduct': 'Bedrijfsvakanties worden niet afgetrokken van vakantiedagen.', + 'vacay.weekStart': 'Week begint op', + 'vacay.weekStartHint': 'Kies of de kalenderweek op maandag of zondag begint', 'vacay.carryOver': 'Overdracht', 'vacay.carryOverHint': 'Draag resterende vakantiedagen automatisch over naar het volgende jaar', 'vacay.sharing': 'Delen', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index e21972d8..5bb8d49a 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -692,6 +692,8 @@ const pl: Record = { 'vacay.companyHolidays': 'Urlopy firmowe', 'vacay.companyHolidaysHint': 'Pozwala oznaczać dni wolne od pracy w kalendarzu', 'vacay.companyHolidaysNoDeduct': 'Urlopy firmowe nie są odejmowane od puli dni urlopowych.', + 'vacay.weekStart': 'Tydzień zaczyna się w', + 'vacay.weekStartHint': 'Wybierz czy tydzień zaczyna się w poniedziałek czy niedzielę', 'vacay.carryOver': 'Przeniesienie na kolejny rok', 'vacay.carryOverHint': 'Automatycznie przenosi pozostałe dni urlopowe na kolejny rok', 'vacay.sharing': 'Udostępnianie', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 073140a2..56d98696 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -719,6 +719,8 @@ const ru: Record = { 'vacay.companyHolidays': 'Корпоративные выходные', 'vacay.companyHolidaysHint': 'Разрешить отмечать корпоративные выходные дни', 'vacay.companyHolidaysNoDeduct': 'Корпоративные выходные не вычитаются из дней отпуска.', + 'vacay.weekStart': 'Неделя начинается с', + 'vacay.weekStartHint': 'Выберите, начинается ли неделя с понедельника или воскресенья', 'vacay.carryOver': 'Перенос', 'vacay.carryOverHint': 'Автоматически переносить оставшиеся дни отпуска на следующий год', 'vacay.sharing': 'Общий доступ', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 206f0da8..f4ad849f 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -719,6 +719,8 @@ const zh: Record = { 'vacay.companyHolidays': '公司假日', 'vacay.companyHolidaysHint': '允许标记公司统一休假日', 'vacay.companyHolidaysNoDeduct': '公司假日不计入年假天数。', + 'vacay.weekStart': '每周开始于', + 'vacay.weekStartHint': '选择日历周从周一还是周日开始', 'vacay.carryOver': '结转', 'vacay.carryOverHint': '自动将剩余年假天数结转到下一年', 'vacay.sharing': '共享', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index 11a5d017..9f901897 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -744,6 +744,8 @@ const zhTw: Record = { 'vacay.companyHolidays': '公司假日', 'vacay.companyHolidaysHint': '允許標記公司統一休假日', 'vacay.companyHolidaysNoDeduct': '公司假日不計入年假天數。', + 'vacay.weekStart': '每週開始於', + 'vacay.weekStartHint': '選擇日曆週從週一還是週日開始', 'vacay.carryOver': '結轉', 'vacay.carryOverHint': '自動將剩餘年假天數結轉到下一年', 'vacay.sharing': '共享', diff --git a/client/src/types.ts b/client/src/types.ts index 159a0edd..bcaa82e6 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -337,6 +337,7 @@ export interface VacayPlan { block_weekends: boolean carry_over_enabled: boolean company_holidays_enabled: boolean + week_start?: number name?: string year?: number owner_id?: number diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts index ec2766d1..f614ff4d 100644 --- a/server/src/db/migrations.ts +++ b/server/src/db/migrations.ts @@ -1431,6 +1431,10 @@ function runMigrations(db: Database.Database): void { `); db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_journey_share_journey ON journey_share_tokens(journey_id)'); }, + // Migration: Vacay week_start setting (0=Sunday, 1=Monday default) + () => { + try { db.exec("ALTER TABLE vacay_plans ADD COLUMN week_start INTEGER NOT NULL DEFAULT 1"); } catch {} + }, ]; if (currentVersion < migrations.length) { diff --git a/server/src/services/vacayService.ts b/server/src/services/vacayService.ts index 497eb3ef..70c6780c 100644 --- a/server/src/services/vacayService.ts +++ b/server/src/services/vacayService.ts @@ -185,10 +185,11 @@ export interface UpdatePlanBody { company_holidays_enabled?: boolean; carry_over_enabled?: boolean; weekend_days?: string; + week_start?: number; } export async function updatePlan(planId: number, body: UpdatePlanBody, socketId: string | undefined) { - const { block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled, weekend_days } = body; + const { block_weekends, holidays_enabled, holidays_region, company_holidays_enabled, carry_over_enabled, weekend_days, week_start } = body; const updates: string[] = []; const params: (string | number)[] = []; @@ -198,6 +199,7 @@ export async function updatePlan(planId: number, body: UpdatePlanBody, socketId: if (company_holidays_enabled !== undefined) { updates.push('company_holidays_enabled = ?'); params.push(company_holidays_enabled ? 1 : 0); } if (carry_over_enabled !== undefined) { updates.push('carry_over_enabled = ?'); params.push(carry_over_enabled ? 1 : 0); } if (weekend_days !== undefined) { updates.push('weekend_days = ?'); params.push(String(weekend_days)); } + if (week_start !== undefined) { updates.push('week_start = ?'); params.push(week_start === 0 ? 0 : 1); } if (updates.length > 0) { params.push(planId);