using Microsoft.EntityFrameworkCore; using timetracker.Shared; namespace timetracker.Data; public class TimetrackerService(IDbContextFactory factory) : ITimetrackerService { public async Task> GetWeekAsync(int userId, DateOnly monday) { await using var db = await factory.CreateDbContextAsync(); return await db.WorkDays .Include(w => w.Breaks) .Where(w => w.UserId == userId && w.Date >= monday && w.Date < monday.AddDays(7)) .OrderBy(w => w.Date) .ToListAsync(); } public async Task UpsertWorkDayAsync(WorkDay workDay) { await using var db = await factory.CreateDbContextAsync(); var existing = await db.WorkDays .Include(w => w.Breaks) .FirstOrDefaultAsync(w => w.UserId == workDay.UserId && w.Date == workDay.Date); if (existing == null) { workDay.Id = 0; foreach (var b in workDay.Breaks) b.Id = 0; db.WorkDays.Add(workDay); } else { existing.StartTime = workDay.StartTime; existing.EndTime = workDay.EndTime; db.BreakEntries.RemoveRange(existing.Breaks); existing.Breaks.Clear(); foreach (var b in workDay.Breaks) existing.Breaks.Add(new BreakEntry { WorkDayId = existing.Id, StartTime = b.StartTime, EndTime = b.EndTime }); } await db.SaveChangesAsync(); } public async Task GetSettingsAsync(int userId) { await using var db = await factory.CreateDbContextAsync(); return await db.AppSettings.FirstOrDefaultAsync(s => s.UserId == userId) ?? new AppSettings { UserId = userId }; } public async Task SaveSettingsAsync(AppSettings settings) { await using var db = await factory.CreateDbContextAsync(); var existing = await db.AppSettings.FirstOrDefaultAsync(s => s.UserId == settings.UserId); if (existing == null) db.AppSettings.Add(settings); else { existing.DailyTargetHours = settings.DailyTargetHours; existing.MinimumBreakMinutes = settings.MinimumBreakMinutes; existing.VacationDaysPerYear = settings.VacationDaysPerYear; existing.WorkMonday = settings.WorkMonday; existing.WorkTuesday = settings.WorkTuesday; existing.WorkWednesday = settings.WorkWednesday; existing.WorkThursday = settings.WorkThursday; existing.WorkFriday = settings.WorkFriday; existing.WorkSaturday = settings.WorkSaturday; existing.WorkSunday = settings.WorkSunday; } await db.SaveChangesAsync(); } // ── Urlaub ──────────────────────────────────────────────────────────── public async Task> GetVacationDaysAsync(int userId, int year) { await using var db = await factory.CreateDbContextAsync(); return await db.VacationDays .Where(v => v.UserId == userId && v.Date.Year == year) .OrderBy(v => v.Date) .ToListAsync(); } public async Task AddVacationDayAsync(VacationDay vacationDay) { await using var db = await factory.CreateDbContextAsync(); var exists = await db.VacationDays.AnyAsync(v => v.UserId == vacationDay.UserId && v.Date == vacationDay.Date); if (!exists) { vacationDay.Id = 0; db.VacationDays.Add(vacationDay); await db.SaveChangesAsync(); } } public async Task RemoveVacationDayAsync(int userId, int id) { await using var db = await factory.CreateDbContextAsync(); var v = await db.VacationDays.FirstOrDefaultAsync(v => v.Id == id && v.UserId == userId); if (v != null) { db.VacationDays.Remove(v); await db.SaveChangesAsync(); } } // ── Gleitzeitkonto ─────────────────────────────────────────────────── public async Task GetTotalOvertimeAsync(int userId, AppSettings settings) { await using var db = await factory.CreateDbContextAsync(); // 1. Finde das Startdatum für die Berechnung var firstDay = await db.WorkDays .Where(w => w.UserId == userId) .OrderBy(w => w.Date) .Select(w => (DateOnly?)w.Date) .FirstOrDefaultAsync(); if (firstDay == null && !settings.FlexTimeStartDate.HasValue) return TimeSpan.FromHours(settings.FlexTimeStartingBalanceHours); var startDate = settings.FlexTimeStartDate ?? firstDay!.Value; var endDate = DateOnly.FromDateTime(DateTime.Today); if (startDate > endDate) return TimeSpan.FromHours(settings.FlexTimeStartingBalanceHours); // 2. Lade alle erfassten Arbeitstage in diesem Zeitraum var workDays = await db.WorkDays .Include(w => w.Breaks) .Where(w => w.UserId == userId && w.Date >= startDate && w.Date <= endDate) .ToDictionaryAsync(w => w.Date); // 3. Lade alle Feiertage in diesem Zeitraum var holidaysList = await db.PublicHolidays .Where(h => h.Date >= startDate && h.Date <= endDate) .ToListAsync(); var state = settings.GermanState; var holidaySet = holidaysList .Where(h => string.IsNullOrEmpty(h.Counties) || (!string.IsNullOrEmpty(state) && h.Counties.Split(',').Contains(state))) .Select(h => h.Date) .ToHashSet(); // 4. Lade alle Urlaubstage in diesem Zeitraum var vacationSet = await db.VacationDays .Where(v => v.UserId == userId && v.Date >= startDate && v.Date <= endDate) .Select(v => v.Date) .ToHashSetAsync(); double totalOvertimeHours = settings.FlexTimeStartingBalanceHours; for (var date = startDate; date <= endDate; date = date.AddDays(1)) { bool isWorkDay = settings.IsWorkDay(date.DayOfWeek); bool isHoliday = holidaySet.Contains(date); bool isVacation = vacationSet.Contains(date); // Sollzeit gilt nur an regulären Arbeitstagen, die weder Feiertag noch Urlaub sind double target = (isWorkDay && !isHoliday && !isVacation) ? settings.DailyTargetHours : 0.0; double actual = 0.0; if (workDays.TryGetValue(date, out var wd) && wd.StartTime != null && wd.EndTime != null) { var gross = wd.EndTime.Value.ToTimeSpan() - wd.StartTime.Value.ToTimeSpan(); if (gross > TimeSpan.Zero) { var breakTotal = wd.Breaks .Where(b => b.StartTime.HasValue && b.EndTime.HasValue && b.EndTime > b.StartTime) .Aggregate(TimeSpan.Zero, (s, b) => s + (b.EndTime!.Value.ToTimeSpan() - b.StartTime!.Value.ToTimeSpan())); actual = (gross - breakTotal).TotalHours; if (actual < 0) actual = 0.0; } } totalOvertimeHours += (actual - target); } return TimeSpan.FromHours(totalOvertimeHours); } // ── Monatsübersicht ─────────────────────────────────────────────────── public async Task> GetMonthAsync(int userId, int year, int month) { await using var db = await factory.CreateDbContextAsync(); var from = new DateOnly(year, month, 1); var to = from.AddMonths(1); return await db.WorkDays .Include(w => w.Breaks) .Where(w => w.UserId == userId && w.Date >= from && w.Date < to) .OrderBy(w => w.Date) .ToListAsync(); } }