fix(vacay): fix entitlement counter, year deletion, and year creation bugs

- toggleCompanyHoliday now calls loadStats() so the entitlement sidebar
  updates immediately when a vacation day is converted to a company holiday
- deleteYear now deletes vacay_user_years rows for the removed year,
  preventing stale entitlement data from persisting and re-appearing
  when the year is re-created
- deleteYear recalculates carry-over for year+1 when year N is deleted,
  using the new actual previous year as the source
- removeYear store action now calls loadStats() so the sidebar reflects
  the recalculated carry-over without requiring a page refresh
- Add prev-year button (+[<] 2026 [>]+) so users can add years going
  backwards after deleting a past year; add vacay.addPrevYear i18n key
  to all 13 supported languages

Closes #371
This commit is contained in:
jubnl
2026-04-03 19:49:58 +02:00
parent f6faaa23b0
commit 6c72295424
16 changed files with 75 additions and 24 deletions

View File

@@ -598,7 +598,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'خطط وأدر أيام الإجازة',
'vacay.settings': 'الإعدادات',
'vacay.year': 'السنة',
'vacay.addYear': 'إضافة سنة',
'vacay.addYear': 'إضافة السنة التالية',
'vacay.addPrevYear': 'إضافة السنة السابقة',
'vacay.removeYear': 'إزالة السنة',
'vacay.removeYearConfirm': 'إزالة {year}؟',
'vacay.removeYearHint': 'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.',

View File

@@ -579,7 +579,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'Planeje e gerencie dias de férias',
'vacay.settings': 'Configurações',
'vacay.year': 'Ano',
'vacay.addYear': 'Adicionar ano',
'vacay.addYear': 'Adicionar próximo ano',
'vacay.addPrevYear': 'Adicionar ano anterior',
'vacay.removeYear': 'Remover ano',
'vacay.removeYearConfirm': 'Remover {year}?',
'vacay.removeYearHint': 'Todas as entradas de férias e feriados da empresa deste ano serão excluídas permanentemente.',

View File

@@ -597,7 +597,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'Plánování a správa dovolené',
'vacay.settings': 'Nastavení',
'vacay.year': 'Rok',
'vacay.addYear': 'Přidat rok',
'vacay.addYear': 'Přidat následující rok',
'vacay.addPrevYear': 'Přidat předchozí rok',
'vacay.removeYear': 'Odebrat rok',
'vacay.removeYearConfirm': 'Odebrat rok {year}?',
'vacay.removeYearHint': 'Všechny záznamy o dovolené a firemní svátky pro tento rok budou trvale smazány.',

View File

@@ -595,7 +595,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'Urlaubstage planen und verwalten',
'vacay.settings': 'Einstellungen',
'vacay.year': 'Jahr',
'vacay.addYear': 'Jahr hinzufügen',
'vacay.addYear': 'Nächstes Jahr hinzufügen',
'vacay.addPrevYear': 'Vorheriges Jahr hinzufügen',
'vacay.removeYear': 'Jahr entfernen',
'vacay.removeYearConfirm': '{year} entfernen?',
'vacay.removeYearHint': 'Alle Urlaubseinträge und Betriebsferien für dieses Jahr werden unwiderruflich gelöscht.',

View File

@@ -592,7 +592,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'Plan and manage vacation days',
'vacay.settings': 'Settings',
'vacay.year': 'Year',
'vacay.addYear': 'Add year',
'vacay.addYear': 'Add next year',
'vacay.addPrevYear': 'Add previous year',
'vacay.removeYear': 'Remove year',
'vacay.removeYearConfirm': 'Remove {year}?',
'vacay.removeYearHint': 'All vacation entries and company holidays for this year will be permanently deleted.',

View File

@@ -572,7 +572,8 @@ const es: Record<string, string> = {
'vacay.subtitle': 'Planifica y gestiona días de vacaciones',
'vacay.settings': 'Ajustes',
'vacay.year': 'Año',
'vacay.addYear': 'Añadir año',
'vacay.addYear': 'Añadir año siguiente',
'vacay.addPrevYear': 'Añadir año anterior',
'vacay.removeYear': 'Eliminar año',
'vacay.removeYearConfirm': '¿Eliminar {year}?',
'vacay.removeYearHint': 'Todas las vacaciones y festivos de empresa de este año se borrarán permanentemente.',

View File

@@ -594,7 +594,8 @@ const fr: Record<string, string> = {
'vacay.subtitle': 'Planifiez et gérez vos jours de congés',
'vacay.settings': 'Paramètres',
'vacay.year': 'Année',
'vacay.addYear': 'Ajouter une année',
'vacay.addYear': 'Ajouter l\'année suivante',
'vacay.addPrevYear': 'Ajouter l\'année précédente',
'vacay.removeYear': 'Supprimer l\'année',
'vacay.removeYearConfirm': 'Supprimer {year} ?',
'vacay.removeYearHint': 'Toutes les entrées de vacances et jours fériés d\'entreprise de cette année seront définitivement supprimés.',

View File

@@ -595,7 +595,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'Szabadságnapok tervezése és kezelése',
'vacay.settings': 'Beállítások',
'vacay.year': 'Év',
'vacay.addYear': 'Év hozzáadása',
'vacay.addYear': 'Következő év hozzáadása',
'vacay.addPrevYear': 'Előző év hozzáadása',
'vacay.removeYear': 'Év eltávolítása',
'vacay.removeYearConfirm': '{year} eltávolítása?',
'vacay.removeYearHint': 'Az adott év összes szabadság-bejegyzése és céges szabadnapja véglegesen törlődik.',

View File

@@ -595,7 +595,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'Pianifica e gestisci i giorni di ferie',
'vacay.settings': 'Impostazioni',
'vacay.year': 'Anno',
'vacay.addYear': 'Aggiungi anno',
'vacay.addYear': 'Aggiungi anno successivo',
'vacay.addPrevYear': 'Aggiungi anno precedente',
'vacay.removeYear': 'Rimuovi anno',
'vacay.removeYearConfirm': 'Rimuovere {year}?',
'vacay.removeYearHint': 'Tutte le voci delle ferie e le ferie aziendali di questo anno verranno eliminate in modo permanente.',

View File

@@ -594,7 +594,8 @@ const nl: Record<string, string> = {
'vacay.subtitle': 'Plan en beheer vakantiedagen',
'vacay.settings': 'Instellingen',
'vacay.year': 'Jaar',
'vacay.addYear': 'Jaar toevoegen',
'vacay.addYear': 'Volgend jaar toevoegen',
'vacay.addPrevYear': 'Vorig jaar toevoegen',
'vacay.removeYear': 'Jaar verwijderen',
'vacay.removeYearConfirm': '{year} verwijderen?',
'vacay.removeYearHint': 'Alle vakantie-invoeren en bedrijfsvakanties voor dit jaar worden permanent verwijderd.',

View File

@@ -558,7 +558,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'vacay.subtitle': 'Planuj i zarządzaj dniami urlopu',
'vacay.settings': 'Ustawienia',
'vacay.year': 'Rok',
'vacay.addYear': 'Dodaj rok',
'vacay.addYear': 'Dodaj następny rok',
'vacay.addPrevYear': 'Dodaj poprzedni rok',
'vacay.removeYear': 'Usuń rok',
'vacay.removeYearConfirm': 'Usunąć {year}?',
'vacay.removeYearHint': 'Wszystkie wpisy dotyczące urlopów oraz dni wolnych w tym roku zostaną trwale usunięte.',

View File

@@ -594,7 +594,8 @@ const ru: Record<string, string> = {
'vacay.subtitle': 'Планируйте и управляйте днями отпуска',
'vacay.settings': 'Настройки',
'vacay.year': 'Год',
'vacay.addYear': 'Добавить год',
'vacay.addYear': 'Добавить следующий год',
'vacay.addPrevYear': 'Добавить предыдущий год',
'vacay.removeYear': 'Удалить год',
'vacay.removeYearConfirm': 'Удалить {year}?',
'vacay.removeYearHint': 'Все записи об отпуске и корпоративные выходные за этот год будут безвозвратно удалены.',

View File

@@ -594,7 +594,8 @@ const zh: Record<string, string> = {
'vacay.subtitle': '规划和管理假期',
'vacay.settings': '设置',
'vacay.year': '年份',
'vacay.addYear': '添加年',
'vacay.addYear': '添加下一年',
'vacay.addPrevYear': '添加上一年',
'vacay.removeYear': '移除年份',
'vacay.removeYearConfirm': '移除 {year}',
'vacay.removeYearHint': '该年度所有假期记录和公司假日将被永久删除。',

View File

@@ -41,11 +41,16 @@ export default function VacayPage(): React.ReactElement {
if (selectedYear) { loadEntries(selectedYear); loadStats(selectedYear); loadHolidays(selectedYear) }
}, [selectedYear])
const handleAddYear = () => {
const handleAddNextYear = () => {
const nextYear = years.length > 0 ? Math.max(...years) + 1 : new Date().getFullYear()
addYear(nextYear)
}
const handleAddPrevYear = () => {
const prevYear = years.length > 0 ? Math.min(...years) - 1 : new Date().getFullYear()
addYear(prevYear)
}
if (loading) {
return (
<div className="min-h-screen" style={{ background: 'var(--bg-primary)' }}>
@@ -62,20 +67,27 @@ export default function VacayPage(): React.ReactElement {
<>
{/* Year Selector */}
<div className="rounded-xl border p-3" style={{ background: 'var(--bg-card)', borderColor: 'var(--border-primary)' }}>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center mb-2">
<span className="text-[11px] font-medium uppercase tracking-wider" style={{ color: 'var(--text-faint)' }}>{t('vacay.year')}</span>
<button onClick={handleAddYear} className="p-0.5 rounded transition-colors" style={{ color: 'var(--text-faint)' }} title={t('vacay.addYear')}>
<Plus size={14} />
</button>
</div>
<div className="flex items-center justify-between mb-2">
<button onClick={() => { const idx = years.indexOf(selectedYear); if (idx > 0) setSelectedYear(years[idx - 1]) }} disabled={years.indexOf(selectedYear) <= 0} className="p-1 lg:p-1 p-2 rounded-lg disabled:opacity-20 transition-colors" style={{ background: 'var(--bg-secondary)' }}>
<ChevronLeft size={16} style={{ color: 'var(--text-muted)' }} />
</button>
<div className="flex items-center gap-1">
<button onClick={handleAddPrevYear} className="p-0.5 rounded transition-colors" style={{ color: 'var(--text-faint)' }} title={t('vacay.addPrevYear')}>
<Plus size={14} />
</button>
<button onClick={() => { const idx = years.indexOf(selectedYear); if (idx > 0) setSelectedYear(years[idx - 1]) }} disabled={years.indexOf(selectedYear) <= 0} className="p-1 rounded-lg disabled:opacity-20 transition-colors" style={{ background: 'var(--bg-secondary)' }}>
<ChevronLeft size={16} style={{ color: 'var(--text-muted)' }} />
</button>
</div>
<span className="text-xl font-bold tabular-nums" style={{ color: 'var(--text-primary)' }}>{selectedYear}</span>
<button onClick={() => { const idx = years.indexOf(selectedYear); if (idx < years.length - 1) setSelectedYear(years[idx + 1]) }} disabled={years.indexOf(selectedYear) >= years.length - 1} className="p-1 lg:p-1 p-2 rounded-lg disabled:opacity-20 transition-colors" style={{ background: 'var(--bg-secondary)' }}>
<ChevronRight size={16} style={{ color: 'var(--text-muted)' }} />
</button>
<div className="flex items-center gap-1">
<button onClick={() => { const idx = years.indexOf(selectedYear); if (idx < years.length - 1) setSelectedYear(years[idx + 1]) }} disabled={years.indexOf(selectedYear) >= years.length - 1} className="p-1 rounded-lg disabled:opacity-20 transition-colors" style={{ background: 'var(--bg-secondary)' }}>
<ChevronRight size={16} style={{ color: 'var(--text-muted)' }} />
</button>
<button onClick={handleAddNextYear} className="p-0.5 rounded transition-colors" style={{ color: 'var(--text-faint)' }} title={t('vacay.addYear')}>
<Plus size={14} />
</button>
</div>
</div>
<div className="grid grid-cols-4 gap-1">
{years.map(y => (

View File

@@ -229,6 +229,7 @@ export const useVacayStore = create<VacayState>((set, get) => ({
: new Date().getFullYear()
}
set(updates)
await get().loadStats()
},
loadEntries: async (year?: number) => {
@@ -246,6 +247,7 @@ export const useVacayStore = create<VacayState>((set, get) => ({
toggleCompanyHoliday: async (date: string) => {
await api.toggleCompanyHoliday(date)
await get().loadEntries()
await get().loadStats()
},
loadStats: async (year?: number) => {

View File

@@ -496,6 +496,30 @@ export function deleteYear(planId: number, year: number, socketId: string | unde
db.prepare('DELETE FROM vacay_years WHERE plan_id = ? AND year = ?').run(planId, year);
db.prepare("DELETE FROM vacay_entries WHERE plan_id = ? AND date LIKE ?").run(planId, `${year}-%`);
db.prepare("DELETE FROM vacay_company_holidays WHERE plan_id = ? AND date LIKE ?").run(planId, `${year}-%`);
db.prepare('DELETE FROM vacay_user_years WHERE plan_id = ? AND year = ?').run(planId, year);
// Recalculate carry-over for year+1 if it exists, since its previous year has changed
const nextYearExists = db.prepare('SELECT id FROM vacay_years WHERE plan_id = ? AND year = ?').get(planId, year + 1);
if (nextYearExists) {
const plan = db.prepare('SELECT * FROM vacay_plans WHERE id = ?').get(planId) as VacayPlan | undefined;
const carryOverEnabled = plan ? !!plan.carry_over_enabled : true;
const users = getPlanUsers(planId);
const prevYear = db.prepare('SELECT year FROM vacay_years WHERE plan_id = ? AND year < ? ORDER BY year DESC LIMIT 1').get(planId, year + 1) as { year: number } | undefined;
for (const u of users) {
let carry = 0;
if (carryOverEnabled && prevYear) {
const prevConfig = db.prepare('SELECT * FROM vacay_user_years WHERE user_id = ? AND plan_id = ? AND year = ?').get(u.id, planId, prevYear.year) as VacayUserYear | undefined;
if (prevConfig) {
const used = (db.prepare("SELECT COUNT(*) as count FROM vacay_entries WHERE user_id = ? AND plan_id = ? AND date LIKE ?").get(u.id, planId, `${prevYear.year}-%`) as { count: number }).count;
const total = prevConfig.vacation_days + prevConfig.carried_over;
carry = Math.max(0, total - used);
}
}
db.prepare('UPDATE vacay_user_years SET carried_over = ? WHERE user_id = ? AND plan_id = ? AND year = ?').run(carry, u.id, planId, year + 1);
}
}
notifyPlanUsers(planId, socketId, 'vacay:settings');
return listYears(planId);
}