199 lines
8.0 KiB
C#
199 lines
8.0 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace timetracker.Data;
|
|
|
|
public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
|
{
|
|
public async Task<List<WorkDay>> 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<AppSettings> 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<List<VacationDay>> 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<TimeSpan> 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<List<WorkDay>> 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();
|
|
}
|
|
}
|