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:
@@ -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': 'سيتم حذف كل إدخالات الإجازات والعطل الخاصة بهذه السنة نهائيًا.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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': 'Все записи об отпуске и корпоративные выходные за этот год будут безвозвратно удалены.',
|
||||
|
||||
@@ -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': '该年度所有假期记录和公司假日将被永久删除。',
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user