feat(vacay): half vacation days (#552) (#1631)

* feat(vacay): support half vacation days (#552)

Log a vacation day as a half day (0.5) as well as a full day and count it
as such against the entitlement. A "Half day" mode joins the calendar
toolbar on desktop and mobile; half days render as a diagonal half-fill in
the day cell, and the used/remaining totals carry the fraction.

Closes #552

* refactor(vacay): half-day as a per-person toggle with a corner badge

The diagonal half-fill collided with the two-person split — one person's
half day looked like two people off. Drop the diagonal: the cell fill keeps
showing who is off, a half day gets a small corner ½ badge, and a hover
tooltip lists who is off on that date and how much (only when a half day is
present). Logging moves from a third mode to a Half-day toggle that applies
to the selected person, on desktop and mobile.

* feat(vacay): mark half days with an orange corner dot

Swap the ½ badge for a small orange dot in the bottom-right corner, mirroring
the blue trip dot — quieter in the grid, and the hover tooltip still spells
out who is on a half day.

* fix(vacay): make the fraction migration idempotent

The crosswalk test replays the migration tail (rewind + re-run), which expects
every later migration to be idempotent. Guard the vacay_entries ADD COLUMN
behind a pragma_table_info check so the second run is a no-op. Also only set
dayVisual's `half` flag when true, so full days keep their exact
{ background, numColor } shape for the mobile day-model unit test.

* style(vacay): nudge the mobile day dots slightly inward

The blue trip dot and orange half-day dot sat right in the cell corner on
mobile; move both a touch toward the centre so they read cleaner.
This commit is contained in:
Maurice
2026-07-20 23:38:47 +02:00
committed by GitHub
parent a3cb783df7
commit 7528706d00
43 changed files with 372 additions and 56 deletions
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'نهاية الأسبوع',
'vacay.modeVacation': 'إجازة',
'vacay.modeCompany': 'عطلة شركة',
'vacay.modeHalf': 'نصف يوم',
'vacay.modeHalfHint': 'تسجيل أنصاف أيام الإجازة',
'vacay.fullDay': 'يوم كامل',
'vacay.entitlement': 'الاستحقاق',
'vacay.entitlementDays': 'الأيام',
'vacay.used': 'المستخدم',
+3
View File
@@ -28,6 +28,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Fim de semana',
'vacay.modeVacation': 'Férias',
'vacay.modeCompany': 'Feriado da empresa',
'vacay.modeHalf': 'Meio dia',
'vacay.modeHalfHint': 'Registrar meio dia de férias',
'vacay.fullDay': 'Dia inteiro',
'vacay.entitlement': 'Direito',
'vacay.entitlementDays': 'Dias',
'vacay.used': 'Usados',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Cap de setmana',
'vacay.modeVacation': 'Vacances',
'vacay.modeCompany': "Festiu de l'empresa",
'vacay.modeHalf': 'Mitja jornada',
'vacay.modeHalfHint': 'Registra mitges jornades de vacances',
'vacay.fullDay': 'Jornada completa',
'vacay.entitlement': 'Dret',
'vacay.entitlementDays': 'Dies',
'vacay.used': 'Utilitzats',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Víkend',
'vacay.modeVacation': 'Dovolená',
'vacay.modeCompany': 'Firemní volno',
'vacay.modeHalf': 'Půlden',
'vacay.modeHalfHint': 'Zapsat půldenní dovolenou',
'vacay.fullDay': 'Celý den',
'vacay.entitlement': 'Nárok',
'vacay.entitlementDays': 'Dní',
'vacay.used': 'Vyčerpáno',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Wochenende',
'vacay.modeVacation': 'Urlaub',
'vacay.modeCompany': 'Betriebsferien',
'vacay.modeHalf': 'Halbtag',
'vacay.modeHalfHint': 'Halbe Urlaubstage eintragen',
'vacay.fullDay': 'Ganzer Tag',
'vacay.entitlement': 'Urlaubsanspruch',
'vacay.entitlementDays': 'Tage',
'vacay.used': 'Weg',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Weekend',
'vacay.modeVacation': 'Vacation',
'vacay.modeCompany': 'Company Holiday',
'vacay.modeHalf': 'Half day',
'vacay.modeHalfHint': 'Log half vacation days',
'vacay.fullDay': 'Full day',
'vacay.entitlement': 'Entitlement',
'vacay.entitlementDays': 'Days',
'vacay.used': 'Used',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Fin de semana',
'vacay.modeVacation': 'Vacaciones',
'vacay.modeCompany': 'Festivo de empresa',
'vacay.modeHalf': 'Medio día',
'vacay.modeHalfHint': 'Registrar medios días de vacaciones',
'vacay.fullDay': 'Día completo',
'vacay.entitlement': 'Derecho',
'vacay.entitlementDays': 'Días',
'vacay.used': 'Usados',
+3
View File
@@ -28,6 +28,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Week-end',
'vacay.modeVacation': 'Vacances',
'vacay.modeCompany': "Jour férié d'entreprise",
'vacay.modeHalf': 'Demi-journée',
'vacay.modeHalfHint': 'Enregistrer des demi-journées de congé',
'vacay.fullDay': 'Journée complète',
'vacay.entitlement': 'Droits',
'vacay.entitlementDays': 'Jours',
'vacay.used': 'Utilisés',
+3
View File
@@ -28,6 +28,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Σαββατοκύριακο',
'vacay.modeVacation': 'Διακοπές',
'vacay.modeCompany': 'Αργία Εταιρείας',
'vacay.modeHalf': 'Μισή μέρα',
'vacay.modeHalfHint': 'Καταχώριση μισής ημέρας άδειας',
'vacay.fullDay': 'Ολόκληρη μέρα',
'vacay.entitlement': 'Δικαίωμα',
'vacay.entitlementDays': 'Ημέρες',
'vacay.used': 'Χρησιμοποιημένες',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Hétvége',
'vacay.modeVacation': 'Szabadság',
'vacay.modeCompany': 'Céges szabadnap',
'vacay.modeHalf': 'Fél nap',
'vacay.modeHalfHint': 'Fél szabadnapok rögzítése',
'vacay.fullDay': 'Egész nap',
'vacay.entitlement': 'Szabadságkeret',
'vacay.entitlementDays': 'nap',
'vacay.used': 'Felhasznált',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Akhir Pekan',
'vacay.modeVacation': 'Cuti',
'vacay.modeCompany': 'Hari Libur Perusahaan',
'vacay.modeHalf': 'Setengah hari',
'vacay.modeHalfHint': 'Catat setengah hari cuti',
'vacay.fullDay': 'Sehari penuh',
'vacay.entitlement': 'Jatah Cuti',
'vacay.entitlementDays': 'Hari',
'vacay.used': 'Terpakai',
+3
View File
@@ -28,6 +28,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Weekend',
'vacay.modeVacation': 'Ferie',
'vacay.modeCompany': 'Ferie aziendali',
'vacay.modeHalf': 'Mezza giornata',
'vacay.modeHalfHint': 'Registra mezze giornate di ferie',
'vacay.fullDay': 'Giornata intera',
'vacay.entitlement': 'Disponibilità',
'vacay.entitlementDays': 'Giorni',
'vacay.used': 'Usati',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': '週末',
'vacay.modeVacation': '休暇',
'vacay.modeCompany': '会社休日',
'vacay.modeHalf': '半休',
'vacay.modeHalfHint': '半休を登録',
'vacay.fullDay': '全休',
'vacay.entitlement': '付与日数',
'vacay.entitlementDays': '日',
'vacay.used': '使用済み',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': '주말',
'vacay.modeVacation': '휴가',
'vacay.modeCompany': '회사 휴일',
'vacay.modeHalf': '반차',
'vacay.modeHalfHint': '반차 등록',
'vacay.fullDay': '종일',
'vacay.entitlement': '휴가 일수',
'vacay.entitlementDays': '일',
'vacay.used': '사용',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Weekend',
'vacay.modeVacation': 'Vakantie',
'vacay.modeCompany': 'Bedrijfsvakantie',
'vacay.modeHalf': 'Halve dag',
'vacay.modeHalfHint': 'Halve vakantiedagen invoeren',
'vacay.fullDay': 'Hele dag',
'vacay.entitlement': 'Recht',
'vacay.entitlementDays': 'Dagen',
'vacay.used': 'Gebruikt',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Weekendowy',
'vacay.modeVacation': 'Urlop',
'vacay.modeCompany': 'Urlop firmowy',
'vacay.modeHalf': 'Pół dnia',
'vacay.modeHalfHint': 'Zapisz pół dnia urlopu',
'vacay.fullDay': 'Cały dzień',
'vacay.entitlement': 'Wymiar',
'vacay.entitlementDays': 'Dni',
'vacay.used': 'Wykorzystane',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Выходные',
'vacay.modeVacation': 'Отпуск',
'vacay.modeCompany': 'Корпоративный выходной',
'vacay.modeHalf': 'Полдня',
'vacay.modeHalfHint': 'Отмечать полдня отпуска',
'vacay.fullDay': 'Полный день',
'vacay.entitlement': 'Право на отпуск',
'vacay.entitlementDays': 'Дни',
'vacay.used': 'Использовано',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Helgen',
'vacay.modeVacation': 'Semester',
'vacay.modeCompany': 'Företagshelg',
'vacay.modeHalf': 'Halvdag',
'vacay.modeHalfHint': 'Registrera halva semesterdagar',
'vacay.fullDay': 'Heldag',
'vacay.entitlement': 'Rättighet',
'vacay.entitlementDays': 'Dagar',
'vacay.used': 'Använt',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Hafta sonu',
'vacay.modeVacation': 'Tatil',
'vacay.modeCompany': 'Şirket Tatili',
'vacay.modeHalf': 'Yarım gün',
'vacay.modeHalfHint': 'Yarım izin günü ekle',
'vacay.fullDay': 'Tam gün',
'vacay.entitlement': 'Yetki',
'vacay.entitlementDays': 'Günler',
'vacay.used': 'Kullanılmış',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Вихідні',
'vacay.modeVacation': 'Відпустка',
'vacay.modeCompany': 'Корпоративний вихідний',
'vacay.modeHalf': 'Пів дня',
'vacay.modeHalfHint': 'Позначати пів дня відпустки',
'vacay.fullDay': 'Повний день',
'vacay.entitlement': 'Право на відпустку',
'vacay.entitlementDays': 'Дні',
'vacay.used': 'Використано',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': 'Ngày cuối tuần',
'vacay.modeVacation': 'Kì nghỉ',
'vacay.modeCompany': 'Kỳ nghỉ của công ty',
'vacay.modeHalf': 'Nửa ngày',
'vacay.modeHalfHint': 'Ghi nửa ngày nghỉ',
'vacay.fullDay': 'Cả ngày',
'vacay.entitlement': 'Quyền lợi',
'vacay.entitlementDays': 'Ngày',
'vacay.used': 'Đã dùng',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': '週末',
'vacay.modeVacation': '休假',
'vacay.modeCompany': '公司假日',
'vacay.modeHalf': '半天',
'vacay.modeHalfHint': '記錄半天假期',
'vacay.fullDay': '全天',
'vacay.entitlement': '年假額度',
'vacay.entitlementDays': '天',
'vacay.used': '已用',
+3
View File
@@ -27,6 +27,9 @@ const vacay: TranslationStrings = {
'vacay.weekend': '周末',
'vacay.modeVacation': '休假',
'vacay.modeCompany': '公司假日',
'vacay.modeHalf': '半天',
'vacay.modeHalfHint': '记录半天假期',
'vacay.fullDay': '全天',
'vacay.entitlement': '年假额度',
'vacay.entitlementDays': '天',
'vacay.used': '已用',
+2
View File
@@ -48,6 +48,8 @@ export type VacayAddYearRequest = z.infer<typeof vacayAddYearRequestSchema>;
export const vacayToggleEntryRequestSchema = z.object({
date: z.string().min(1),
target_user_id: z.union([z.number(), z.string()]).optional(),
// Half vacation days (#552): 0.5 logs a half day, 1 (or omitted) a full day.
fraction: z.union([z.literal(0.5), z.literal(1)]).optional(),
});
export type VacayToggleEntryRequest = z.infer<typeof vacayToggleEntryRequestSchema>;