9 Commits

Author SHA1 Message Date
Wieland, Marc 8f6d070e6b Merge branch 'main' of https://gitea.marc-wieland.de/mrcwlnd/timetracker 2026-05-22 10:28:34 +02:00
Wieland, Marc 7ab824e7c1 Auth integration 2026-05-22 10:28:02 +02:00
mrcwlnd 73599c00d1 docker-compose.yml aktualisiert 2026-05-22 07:36:19 +00:00
mrcwlnd 64c5f6aa2c docker-compose.yml aktualisiert 2026-05-22 07:29:54 +00:00
mrcwlnd 5431618d43 Dockerfile aktualisiert 2026-05-22 07:29:38 +00:00
mrcwlnd 598243ddc9 docker-compose.yml hinzugefügt 2026-05-22 07:27:32 +00:00
Wieland, Marc 9fd50f86c0 added dockerfile 2 2026-05-22 09:22:47 +02:00
Wieland, Marc 0467f45036 Added dockerfile 2026-05-22 09:21:57 +02:00
Wieland, Marc 88ac175190 first commit 2026-05-22 09:18:01 +02:00
386 changed files with 6821 additions and 2263 deletions
-11
View File
@@ -1,11 +0,0 @@
# SQLite Datenbanken und Journal-Dateien
*.db
*.db-shm
*.db-wal
# Build-Artefakte
bin/
obj/
*.user
*.suo
*.vs/
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">
<head>
@@ -13,11 +13,12 @@
<ImportMap />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="alternate icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveWebAssembly" />
<HeadOutlet />
</head>
<body>
<Routes />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
</body>
@@ -1,9 +1,4 @@
@inherits LayoutComponentBase
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Nav
@inject IUserNotificationService UserNotificationService
@implements IDisposable
@using System.Security.Claims
@inherits LayoutComponentBase
<MudThemeProvider Theme="_theme" />
<MudPopoverProvider />
@@ -34,26 +29,6 @@
@code {
private bool _drawerOpen = true;
protected override void OnInitialized()
{
UserNotificationService.OnUserDeleted += HandleUserDeleted;
}
private async Task HandleUserDeleted(int deletedUserId)
{
var state = await AuthStateProvider.GetAuthenticationStateAsync();
var idClaim = state.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (idClaim != null && int.TryParse(idClaim, out var myId) && myId == deletedUserId)
{
await InvokeAsync(() => Nav.NavigateTo("/auth/logout", forceLoad: true));
}
}
public void Dispose()
{
UserNotificationService.OnUserDeleted -= HandleUserDeleted;
}
private readonly MudTheme _theme = new()
{
PaletteLight = new PaletteLight
+20
View File
@@ -0,0 +1,20 @@
<MudNavMenu>
<MudText Typo="Typo.h6" Class="px-4 mt-4 mb-2">Navigation</MudText>
<MudDivider Class="mb-2" />
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.CalendarMonth">Wochenübersicht</MudNavLink>
<MudNavLink Href="month" Icon="@Icons.Material.Filled.CalendarViewMonth">Monatsübersicht</MudNavLink>
<MudNavLink Href="feiertage" Icon="@Icons.Material.Filled.Celebration">Feiertage</MudNavLink>
<MudNavLink Href="urlaub-maximizer" Icon="@Icons.Material.Filled.AutoAwesome">Urlaubs-Maximizer</MudNavLink>
<MudNavLink Href="settings" Icon="@Icons.Material.Filled.Settings">Einstellungen</MudNavLink>
<MudDivider Class="mt-2 mb-2" />
<AuthorizeView>
<Authorized>
<MudStack Row="true" AlignItems="AlignItems.Center" Class="px-4 py-1" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccountCircle" Color="Color.Primary" Size="Size.Small" />
<MudText Typo="Typo.body2" Style="font-weight:600">@context.User.Identity?.Name</MudText>
</MudStack>
<MudNavLink Href="/auth/logout" Icon="@Icons.Material.Filled.Logout">Abmelden</MudNavLink>
</Authorized>
</AuthorizeView>
</MudNavMenu>
+31
View File
@@ -0,0 +1,31 @@
<script type="module" src="@Assets["Components/Layout/ReconnectModal.razor.js"]"></script>
<dialog id="components-reconnect-modal" data-nosnippet>
<div class="components-reconnect-container">
<div class="components-rejoining-animation" aria-hidden="true">
<div></div>
<div></div>
</div>
<p class="components-reconnect-first-attempt-visible">
Rejoining the server...
</p>
<p class="components-reconnect-repeated-attempt-visible">
Rejoin failed... trying again in <span id="components-seconds-to-next-attempt"></span> seconds.
</p>
<p class="components-reconnect-failed-visible">
Failed to rejoin.<br />Please retry or reload the page.
</p>
<button id="components-reconnect-button" class="components-reconnect-failed-visible">
Retry
</button>
<p class="components-pause-visible">
The session has been paused by the server.
</p>
<p class="components-resume-failed-visible">
Failed to resume the session.<br />Please retry or reload the page.
</p>
<button id="components-resume-button" class="components-pause-visible components-resume-failed-visible">
Resume
</button>
</div>
</dialog>
+157
View File
@@ -0,0 +1,157 @@
.components-reconnect-first-attempt-visible,
.components-reconnect-repeated-attempt-visible,
.components-reconnect-failed-visible,
.components-pause-visible,
.components-resume-failed-visible,
.components-rejoining-animation {
display: none;
}
#components-reconnect-modal.components-reconnect-show .components-reconnect-first-attempt-visible,
#components-reconnect-modal.components-reconnect-show .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-paused .components-pause-visible,
#components-reconnect-modal.components-reconnect-resume-failed .components-resume-failed-visible,
#components-reconnect-modal.components-reconnect-retrying,
#components-reconnect-modal.components-reconnect-retrying .components-reconnect-repeated-attempt-visible,
#components-reconnect-modal.components-reconnect-retrying .components-rejoining-animation,
#components-reconnect-modal.components-reconnect-failed,
#components-reconnect-modal.components-reconnect-failed .components-reconnect-failed-visible {
display: block;
}
#components-reconnect-modal {
background-color: white;
width: 20rem;
margin: 20vh auto;
padding: 2rem;
border: 0;
border-radius: 0.5rem;
box-shadow: 0 3px 6px 2px rgba(0, 0, 0, 0.3);
opacity: 0;
transition: display 0.5s allow-discrete, overlay 0.5s allow-discrete;
animation: components-reconnect-modal-fadeOutOpacity 0.5s both;
&[open]
{
animation: components-reconnect-modal-slideUp 1.5s cubic-bezier(.05, .89, .25, 1.02) 0.3s, components-reconnect-modal-fadeInOpacity 0.5s ease-in-out 0.3s;
animation-fill-mode: both;
}
}
#components-reconnect-modal::backdrop {
background-color: rgba(0, 0, 0, 0.4);
animation: components-reconnect-modal-fadeInOpacity 0.5s ease-in-out;
opacity: 1;
}
@keyframes components-reconnect-modal-slideUp {
0% {
transform: translateY(30px) scale(0.95);
}
100% {
transform: translateY(0);
}
}
@keyframes components-reconnect-modal-fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes components-reconnect-modal-fadeOutOpacity {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.components-reconnect-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
#components-reconnect-modal p {
margin: 0;
text-align: center;
}
#components-reconnect-modal button {
border: 0;
background-color: #6b9ed2;
color: white;
padding: 4px 24px;
border-radius: 4px;
}
#components-reconnect-modal button:hover {
background-color: #3b6ea2;
}
#components-reconnect-modal button:active {
background-color: #6b9ed2;
}
.components-rejoining-animation {
position: relative;
width: 80px;
height: 80px;
}
.components-rejoining-animation div {
position: absolute;
border: 3px solid #0087ff;
opacity: 1;
border-radius: 50%;
animation: components-rejoining-animation 1.5s cubic-bezier(0, 0.2, 0.8, 1) infinite;
}
.components-rejoining-animation div:nth-child(2) {
animation-delay: -0.5s;
}
@keyframes components-rejoining-animation {
0% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
4.9% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 0;
}
5% {
top: 40px;
left: 40px;
width: 0;
height: 0;
opacity: 1;
}
100% {
top: 0px;
left: 0px;
width: 80px;
height: 80px;
opacity: 0;
}
}
+63
View File
@@ -0,0 +1,63 @@
// Set up event handlers
const reconnectModal = document.getElementById("components-reconnect-modal");
reconnectModal.addEventListener("components-reconnect-state-changed", handleReconnectStateChanged);
const retryButton = document.getElementById("components-reconnect-button");
retryButton.addEventListener("click", retry);
const resumeButton = document.getElementById("components-resume-button");
resumeButton.addEventListener("click", resume);
function handleReconnectStateChanged(event) {
if (event.detail.state === "show") {
reconnectModal.showModal();
} else if (event.detail.state === "hide") {
reconnectModal.close();
} else if (event.detail.state === "failed") {
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
} else if (event.detail.state === "rejected") {
location.reload();
}
}
async function retry() {
document.removeEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
try {
// Reconnect will asynchronously return:
// - true to mean success
// - false to mean we reached the server, but it rejected the connection (e.g., unknown circuit ID)
// - exception to mean we didn't reach the server (this can be sync or async)
const successful = await Blazor.reconnect();
if (!successful) {
// We have been able to reach the server, but the circuit is no longer available.
// We'll reload the page so the user can continue using the app as quickly as possible.
const resumeSuccessful = await Blazor.resumeCircuit();
if (!resumeSuccessful) {
location.reload();
} else {
reconnectModal.close();
}
}
} catch (err) {
// We got an exception, server is currently unavailable
document.addEventListener("visibilitychange", retryWhenDocumentBecomesVisible);
}
}
async function resume() {
try {
const successful = await Blazor.resumeCircuit();
if (!successful) {
location.reload();
}
} catch {
reconnectModal.classList.replace("components-reconnect-paused", "components-reconnect-resume-failed");
}
}
async function retryWhenDocumentBecomesVisible() {
if (document.visibilityState === "visible") {
await retry();
}
}
@@ -1,9 +1,7 @@
@page "/feiertage"
@rendermode InteractiveWebAssembly
@rendermode InteractiveServer
@attribute [Authorize]
@inject IHolidayService HolidayService
@inject ITimetrackerService TrackerService
@inject AuthenticationStateProvider AuthStateProvider
@inject HolidayService HolidayService
<PageTitle>Feiertage Timetracker</PageTitle>
@@ -188,25 +186,15 @@ else
private List<PublicHoliday> _holidays = [];
private string _subLabel = "";
private int _userId;
private AppSettings _settings = new();
protected override async Task OnInitializedAsync()
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var claim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
if (claim != null)
{
_userId = int.Parse(claim.Value);
_settings = await TrackerService.GetSettingsAsync(_userId);
}
await LoadHolidays();
_loading = false;
}
private async Task LoadHolidays()
{
_holidays = await HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
_holidays = await HolidayService.GetHolidaysAsync(_year);
var count = _holidays.Count;
var past = _holidays.Count(h => h.Date < DateOnly.FromDateTime(DateTime.Today));
_subLabel = count == 0
@@ -1,8 +1,8 @@
@page "/"
@rendermode InteractiveWebAssembly
@page "/"
@rendermode InteractiveServer
@attribute [Authorize]
@inject ITimetrackerService TrackerService
@inject IHolidayService HolidayService
@inject TimetrackerService TrackerService
@inject HolidayService HolidayService
@inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
@@ -358,39 +358,20 @@ else
_userId = int.Parse(claim.Value);
_monday = GetMonday(DateOnly.FromDateTime(DateTime.Today));
_settings = await TrackerService.GetSettingsAsync(_userId);
var loadWeekTask = LoadWeek();
var overtimeTask = TrackerService.GetTotalOvertimeAsync(_userId, _settings);
await Task.WhenAll(loadWeekTask, overtimeTask);
_totalOvertime = await overtimeTask;
await LoadWeek();
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
_loading = false;
}
private async Task LoadWeek()
{
Task<List<PublicHoliday>> holidaysTask;
if (_monday.Year != _holidayYear)
{
holidaysTask = HolidayService.GetHolidaysAsync(_monday.Year, _settings.GermanState);
}
else
{
holidaysTask = Task.FromResult(new List<PublicHoliday>());
}
var dbDaysTask = TrackerService.GetWeekAsync(_userId, _monday);
await Task.WhenAll(holidaysTask, dbDaysTask);
if (_monday.Year != _holidayYear)
{
var list = await holidaysTask;
var list = await HolidayService.GetHolidaysAsync(_monday.Year);
_holidays = list.ToDictionary(h => h.Date, h => h.Name);
_holidayYear = _monday.Year;
}
var dbDays = await dbDaysTask;
var dbDays = await TrackerService.GetWeekAsync(_userId, _monday);
_days = Enumerable.Range(0, 7).Select(i =>
{
var date = _monday.AddDays(i);
+130
View File
@@ -0,0 +1,130 @@
@page "/login"
@rendermode InteractiveServer
@attribute [AllowAnonymous]
@inject NavigationManager Nav
@inject ISnackbar Snackbar
<PageTitle>Anmelden Timetracker</PageTitle>
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16">
<MudStack AlignItems="AlignItems.Center" Spacing="4">
@* ── Logo / Header ── *@
<MudStack AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccessTime"
Style="font-size:4rem; color:#1565C0" />
<MudText Typo="Typo.h4" Style="font-weight:700; color:#1565C0">Timetracker</MudText>
</MudStack>
<MudPaper Elevation="4" Class="pa-6 rounded-xl" Style="width:100%">
<MudTabs @bind-ActivePanelIndex="_activeTab" Rounded="true" Centered="true" Color="Color.Primary">
@* ── Login ── *@
<MudTabPanel Text="Anmelden">
<MudStack Spacing="3" Class="mt-4">
@if (_error != null && _activeTab == 0)
{
<MudAlert Severity="Severity.Error" Dense="true">@_error</MudAlert>
}
<form action="/auth/login" method="post">
<MudStack Spacing="3">
<MudTextField @bind-Value="_loginUsername"
Label="Benutzername"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person"
name="username"
AutoFocus="true" />
<MudTextField @bind-Value="_loginPassword"
Label="Passwort"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock"
InputType="@(_showLoginPw ? InputType.Text : InputType.Password)"
AdornmentAriaLabel="Passwort anzeigen"
name="password"
OnAdornmentClick="@(() => _showLoginPw = !_showLoginPw)" />
<MudButton ButtonType="ButtonType.Submit"
Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.Login">
Anmelden
</MudButton>
</MudStack>
</form>
</MudStack>
</MudTabPanel>
@* ── Registrieren ── *@
<MudTabPanel Text="Registrieren">
<MudStack Spacing="3" Class="mt-4">
@if (_error != null && _activeTab == 1)
{
<MudAlert Severity="Severity.Error" Dense="true">@_error</MudAlert>
}
<form action="/auth/register" method="post">
<MudStack Spacing="3">
<MudTextField @bind-Value="_regUsername"
Label="Benutzername"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person"
name="username"
HelperText="Mindestens 3 Zeichen" />
<MudTextField @bind-Value="_regPassword"
Label="Passwort"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock"
InputType="@(_showRegPw ? InputType.Text : InputType.Password)"
AdornmentAriaLabel="Passwort anzeigen"
name="password"
HelperText="Mindestens 6 Zeichen"
OnAdornmentClick="@(() => _showRegPw = !_showRegPw)" />
<MudButton ButtonType="ButtonType.Submit"
Variant="Variant.Filled"
Color="Color.Secondary"
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.PersonAdd">
Konto erstellen
</MudButton>
</MudStack>
</form>
</MudStack>
</MudTabPanel>
</MudTabs>
</MudPaper>
</MudStack>
</MudContainer>
@code {
private int _activeTab = 0;
private string? _error;
private string _loginUsername = "";
private string _loginPassword = "";
private string _regUsername = "";
private string _regPassword = "";
private bool _showLoginPw;
private bool _showRegPw;
[SupplyParameterFromQuery(Name = "error")]
public string? ErrorParam { get; set; }
[SupplyParameterFromQuery(Name = "tab")]
public string? TabParam { get; set; }
protected override void OnParametersSet()
{
_error = ErrorParam switch
{
"invalid" => "Benutzername oder Passwort falsch.",
not null => Uri.UnescapeDataString(ErrorParam),
_ => null
};
_activeTab = TabParam == "register" ? 1 : 0;
}
}
@@ -1,8 +1,8 @@
@page "/month"
@rendermode InteractiveWebAssembly
@rendermode InteractiveServer
@attribute [Authorize]
@inject ITimetrackerService TrackerService
@inject IHolidayService HolidayService
@inject TimetrackerService TrackerService
@inject HolidayService HolidayService
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year Monatsübersicht Timetracker</PageTitle>
@@ -181,15 +181,9 @@ else
private async Task LoadMonth()
{
var workDaysTask = TrackerService.GetMonthAsync(_userId, _year, _month);
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year);
await Task.WhenAll(workDaysTask, holidaysTask, vacationsTask);
var workDays = await workDaysTask;
var holidays = await holidaysTask;
var vacations = await vacationsTask;
var workDays = await TrackerService.GetMonthAsync(_userId, _year, _month);
var holidays = await HolidayService.GetHolidaysAsync(_year);
var vacations = await TrackerService.GetVacationDaysAsync(_userId, _year);
var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name);
var vacationSet = vacations.Select(v => v.Date).ToHashSet();
@@ -1,8 +1,8 @@
@page "/settings"
@rendermode InteractiveWebAssembly
@page "/settings"
@rendermode InteractiveServer
@attribute [Authorize]
@inject ITimetrackerService TrackerService
@inject IHolidayService HolidayService
@inject TimetrackerService TrackerService
@inject HolidayService HolidayService
@inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
@@ -128,65 +128,6 @@ else
</MudCard>
</MudItem>
@* ── Region & Feiertage ── *@
<MudItem xs="12" md="6">
<MudCard Elevation="3" Class="rounded-xl h-100">
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.Public" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Region & Feiertage</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="4">
<MudText Typo="Typo.body2" Color="Color.Secondary">
Wähle dein Bundesland aus, um Bundesland-spezifische Feiertage zu berücksichtigen.
</MudText>
<MudSelect T="string" Label="Bundesland" Variant="Variant.Outlined" @bind-Value="_settings.GermanState" Clearable="true" Placeholder="Nur bundesweite Feiertage">
@foreach (var state in GermanStates)
{
<MudSelectItem Value="@state.Key">@state.Value</MudSelectItem>
}
</MudSelect>
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
@* ── Gleitzeitkonto ── *@
<MudItem xs="12" md="6">
<MudCard Elevation="3" Class="rounded-xl h-100">
<MudCardHeader>
<CardHeaderContent>
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.AccountBalance" Color="Color.Primary" />
<MudText Typo="Typo.h6" Style="font-weight:600">Gleitzeitkonto-Start</MudText>
</MudStack>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudStack Spacing="4">
<MudDatePicker Label="Berechnungsstart"
@bind-Date="_flexStartDate"
HelperText="Gleitzeitberechnung läuft ab diesem Datum. Wenn leer, ab dem ersten Arbeitseintrag."
Variant="Variant.Outlined"
Clearable="true"
DateFormat="dd.MM.yyyy"
PickerVariant="PickerVariant.Inline" />
<MudNumericField @bind-Value="_settings.FlexTimeStartingBalanceHours"
Label="Anfangsüberstunden (h)"
Variant="Variant.Outlined"
Step="0.5"
Format="0.##"
HelperText="Stufensaldo (Guthaben/Schulden) zum Berechnungsstart" />
</MudStack>
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
@* ── Speichern-Button ── *@
@@ -437,32 +378,6 @@ else
private AppSettings? _settings;
private int _userId;
private DateTime? _flexStartDate
{
get => _settings?.FlexTimeStartDate?.ToDateTime(TimeOnly.MinValue);
set => _settings!.FlexTimeStartDate = value.HasValue ? DateOnly.FromDateTime(value.Value) : null;
}
private static readonly Dictionary<string, string> GermanStates = new()
{
{ "DE-BW", "Baden-Württemberg" },
{ "DE-BY", "Bayern" },
{ "DE-BE", "Berlin" },
{ "DE-BB", "Brandenburg" },
{ "DE-HB", "Bremen" },
{ "DE-HH", "Hamburg" },
{ "DE-HE", "Hessen" },
{ "DE-MV", "Mecklenburg-Vorpommern" },
{ "DE-NI", "Niedersachsen" },
{ "DE-NW", "Nordrhein-Westfalen" },
{ "DE-RP", "Rheinland-Pfalz" },
{ "DE-SL", "Saarland" },
{ "DE-SN", "Sachsen" },
{ "DE-ST", "Sachsen-Anhalt" },
{ "DE-SH", "Schleswig-Holstein" },
{ "DE-TH", "Thüringen" }
};
private int _vacYear = DateTime.Today.Year;
private List<VacationDay> _vacationDays = [];
private DateTime? _newVacDateFrom;
@@ -495,12 +410,8 @@ else
if (claim == null) return;
_userId = int.Parse(claim.Value);
_settings = await TrackerService.GetSettingsAsync(_userId);
var loadVacationsTask = LoadVacations();
var loadHolidaysTask = HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
await Task.WhenAll(loadVacationsTask, loadHolidaysTask);
_holHolidays = await loadHolidaysTask;
await LoadVacations();
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
}
private async Task LoadVacations()
@@ -518,8 +429,6 @@ else
{
if (_settings == null) return;
await TrackerService.SaveSettingsAsync(_settings);
// Nach dem Speichern Feiertage neu laden, falls sich das Bundesland geändert hat
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
Snackbar.Add("Einstellungen gespeichert", Severity.Success);
}
@@ -559,14 +468,14 @@ else
private async Task ChangeHolYear(int delta)
{
_holYear += delta;
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
}
private async Task FetchHolidays()
{
_fetchingHolidays = true;
var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
_fetchingHolidays = false;
Snackbar.Add(message, success ? Severity.Success : Severity.Error);
}
@@ -574,7 +483,7 @@ else
private async Task DeleteHoliday(int id)
{
await HolidayService.DeleteAsync(id);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
Snackbar.Add("Feiertag entfernt", Severity.Info);
}
@@ -1,8 +1,8 @@
@page "/urlaub-maximizer"
@rendermode InteractiveWebAssembly
@rendermode InteractiveServer
@attribute [Authorize]
@inject ITimetrackerService TrackerService
@inject IHolidayService HolidayService
@inject TimetrackerService TrackerService
@inject HolidayService HolidayService
@inject ISnackbar Snackbar
@inject AuthenticationStateProvider AuthStateProvider
@@ -249,13 +249,8 @@ else
private async Task LoadYear()
{
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year);
await Task.WhenAll(holidaysTask, vacationsTask);
var holidays = await holidaysTask;
var vacations = await vacationsTask;
var holidays = await HolidayService.GetHolidaysAsync(_year);
var vacations = await TrackerService.GetVacationDaysAsync(_userId, _year);
_holidays = holidays.ToDictionary(h => h.Date, h => h.Name);
_vacationSet = vacations.Select(v => v.Date).ToHashSet();
_remainingDays = Math.Max(0, _settings.VacationDaysPerYear - vacations.Count);
@@ -1,10 +1,10 @@
@rendermode InteractiveWebAssembly
@rendermode InteractiveServer
<Router AppAssembly="typeof(timetracker.Client._Imports).Assembly">
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(timetracker.Client.Components.Layout.MainLayout)">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)">
<NotAuthorized>
<timetracker.Client.Components.RedirectToLogin />
<RedirectToLogin />
</NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
@@ -1,4 +1,4 @@
@using System.Net.Http
@using System.Net.Http
@using System.Net.Http.Json
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@@ -10,8 +10,7 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using timetracker
@using timetracker.Client.Components
@using timetracker.Client.Components.Layout
@using timetracker.Components
@using timetracker.Components.Layout
@using timetracker.Data
@using timetracker.Shared
@using MudBlazor
@@ -1,4 +1,4 @@
namespace timetracker.Shared;
namespace timetracker.Data;
public class AppSettings
{
@@ -7,9 +7,6 @@ public class AppSettings
public double DailyTargetHours { get; set; } = 7.5;
public int MinimumBreakMinutes { get; set; } = 30;
public int VacationDaysPerYear { get; set; } = 30;
public string? GermanState { get; set; }
public DateOnly? FlexTimeStartDate { get; set; }
public double FlexTimeStartingBalanceHours { get; set; } = 0.0;
// Arbeitstage
public bool WorkMonday { get; set; } = true;
@@ -1,11 +1,10 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using timetracker.Shared;
namespace timetracker.Data;
public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNotificationService notifier) : IAuthService
public class AuthService(IDbContextFactory<TimetrackerDbContext> factory)
{
public async Task<User?> LoginAsync(string username, string password)
{
@@ -16,46 +15,8 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
return VerifyPassword(password, user.PasswordHash, user.PasswordSalt) ? user : null;
}
public async Task<List<User>> GetAllUsersAsync()
public async Task<(User? User, string? Error)> RegisterAsync(string username, string password)
{
await using var db = await factory.CreateDbContextAsync();
return await db.Users
.OrderBy(u => u.Username)
.ToListAsync();
}
public async Task DeleteUserAsync(int userId)
{
await using var db = await factory.CreateDbContextAsync();
var user = await db.Users.FindAsync(userId);
if (user != null)
{
db.Users.Remove(user);
await db.SaveChangesAsync();
await notifier.NotifyUserDeletedAsync(userId);
await notifier.NotifyUsersChangedAsync();
}
}
public async Task<string?> RenameUserAsync(int userId, string newUsername)
{
if (string.IsNullOrWhiteSpace(newUsername) || newUsername.Length < 3)
return "Benutzername muss mindestens 3 Zeichen lang sein.";
await using var db = await factory.CreateDbContextAsync();
if (await db.Users.AnyAsync(u => u.Username == newUsername && u.Id != userId))
return "Benutzername bereits vergeben.";
var user = await db.Users.FindAsync(userId);
if (user == null) return "Benutzer nicht gefunden.";
user.Username = newUsername;
await db.SaveChangesAsync();
await notifier.NotifyUsersChangedAsync();
return null;
}
public async Task<(User? User, string? Error)> RegisterAsync(string username, string password) {
if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
return (null, "Benutzername muss mindestens 3 Zeichen lang sein.");
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
@@ -69,7 +30,6 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
var user = new User { Username = username, PasswordHash = hash, PasswordSalt = salt };
db.Users.Add(user);
await db.SaveChangesAsync();
await notifier.NotifyUsersChangedAsync();
return (user, null);
}
@@ -1,10 +1,9 @@
namespace timetracker.Shared;
namespace timetracker.Data;
public class BreakEntry
{
public int Id { get; set; }
public int WorkDayId { get; set; }
[System.Text.Json.Serialization.JsonIgnore]
public WorkDay WorkDay { get; set; } = null!;
public TimeOnly? StartTime { get; set; }
public TimeOnly? EndTime { get; set; }
@@ -1,30 +1,20 @@
using Microsoft.EntityFrameworkCore;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using timetracker.Shared;
namespace timetracker.Data;
public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, HttpClient http) : IHolidayService
public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, HttpClient http)
{
private const string ApiUrl = "https://date.nager.at/api/v3/PublicHolidays/{0}/DE";
public async Task<List<PublicHoliday>> GetHolidaysAsync(int year, string? stateCode = null)
public async Task<List<PublicHoliday>> GetHolidaysAsync(int year)
{
await using var db = await factory.CreateDbContextAsync();
var holidays = await db.PublicHolidays
return await db.PublicHolidays
.Where(h => h.Date.Year == year)
.OrderBy(h => h.Date)
.ToListAsync();
if (string.IsNullOrEmpty(stateCode))
{
// Default: return only global holidays (where Counties is null or empty)
return holidays.Where(h => string.IsNullOrEmpty(h.Counties)).ToList();
}
// Return global holidays OR holidays that match the user's state code
return holidays.Where(h => string.IsNullOrEmpty(h.Counties) || h.Counties.Split(',').Contains(stateCode)).ToList();
}
public async Task<(bool Success, string Message)> FetchAndStoreAsync(int year)
@@ -44,8 +34,7 @@ public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, Htt
.Select(h => new PublicHoliday
{
Date = DateOnly.Parse(h.Date),
Name = h.LocalName,
Counties = h.Counties != null && h.Counties.Count > 0 ? string.Join(",", h.Counties) : null
Name = h.LocalName
}));
await db.SaveChangesAsync();
@@ -75,9 +64,5 @@ public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, Htt
[JsonPropertyName("localName")]
public string LocalName { get; set; } = "";
[JsonPropertyName("counties")]
public List<string>? Counties { get; set; }
}
}
@@ -26,15 +26,6 @@ namespace timetracker.Data.Migrations
b.Property<double>("DailyTargetHours")
.HasColumnType("REAL");
b.Property<DateOnly?>("FlexTimeStartDate")
.HasColumnType("TEXT");
b.Property<double>("FlexTimeStartingBalanceHours")
.HasColumnType("REAL");
b.Property<string>("GermanState")
.HasColumnType("TEXT");
b.Property<int>("MinimumBreakMinutes")
.HasColumnType("INTEGER");
@@ -98,9 +89,6 @@ namespace timetracker.Data.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Counties")
.HasColumnType("TEXT");
b.Property<DateOnly>("Date")
.HasColumnType("TEXT");
@@ -1,9 +1,8 @@
namespace timetracker.Shared;
namespace timetracker.Data;
public class PublicHoliday
{
public int Id { get; set; }
public DateOnly Date { get; set; }
public string Name { get; set; } = "";
public string? Counties { get; set; }
}
@@ -1,5 +1,4 @@
using Microsoft.EntityFrameworkCore;
using timetracker.Shared;
namespace timetracker.Data;
@@ -1,9 +1,8 @@
using Microsoft.EntityFrameworkCore;
using timetracker.Shared;
namespace timetracker.Data;
public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory) : ITimetrackerService
public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
{
public async Task<List<WorkDay>> GetWeekAsync(int userId, DateOnly monday)
{
@@ -112,76 +111,24 @@ public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
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
var allDays = 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)
.Where(w => w.UserId == userId && w.StartTime != null && w.EndTime != null)
.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 total = TimeSpan.Zero;
foreach (var wd in allDays)
{
if (!settings.IsWorkDay(wd.Date.DayOfWeek)) continue;
var gross = wd.EndTime!.Value.ToTimeSpan() - wd.StartTime!.Value.ToTimeSpan();
if (gross <= TimeSpan.Zero) continue;
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;
total += gross - breakTotal - TimeSpan.FromHours(settings.DailyTargetHours);
}
}
totalOvertimeHours += (actual - target);
}
return TimeSpan.FromHours(totalOvertimeHours);
return total;
}
// ── Monatsübersicht ───────────────────────────────────────────────────
+1 -1
View File
@@ -1,4 +1,4 @@
namespace timetracker.Shared;
namespace timetracker.Data;
public class User
{
@@ -1,4 +1,4 @@
namespace timetracker.Shared;
namespace timetracker.Data;
public class VacationDay
{
@@ -1,4 +1,4 @@
namespace timetracker.Shared;
namespace timetracker.Data;
public class WorkDay
{
+7 -19
View File
@@ -2,35 +2,23 @@
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy project files for restoring dependencies
COPY timetracker.slnx ./
COPY timetracker.Server/timetracker.Server.csproj timetracker.Server/
COPY timetracker.Client/timetracker.Client.csproj timetracker.Client/
COPY timetracker.Shared/timetracker.Shared.csproj timetracker.Shared/
# Keine Unterordner mehr beim Kopieren!
COPY timetracker.csproj ./
RUN dotnet restore timetracker.csproj
# Restore dependencies
RUN dotnet restore timetracker.slnx
# Copy the rest of the source code
COPY timetracker.Server/ timetracker.Server/
COPY timetracker.Client/ timetracker.Client/
COPY timetracker.Shared/ timetracker.Shared/
# Publish
WORKDIR /src/timetracker.Server
RUN dotnet publish -c Release -o /app/publish
COPY . ./
RUN dotnet publish -c Release -o /app/publish --no-restore
# ── Runtime Stage ─────────────────────────────────────────────────────────────
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
# Directory for SQLite database
# Verzeichnis für die SQLite-Datenbank
RUN mkdir -p /data
ENV ASPNETCORE_HTTP_PORTS=8080
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
ENV TIMETRACKER_DB_PATH=/data/timetracker.db
ENV EnableHttpsRedirect=false
@@ -38,4 +26,4 @@ EXPOSE 8080
VOLUME ["/data"]
ENTRYPOINT ["dotnet", "timetracker.Server.dll"]
ENTRYPOINT ["dotnet", "timetracker.dll"]
+114
View File
@@ -0,0 +1,114 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using timetracker.Components;
using timetracker.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/auth/logout";
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
});
builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<AuthService>();
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddMudServices();
builder.Services.AddHttpClient<HolidayService>();
var dbPath = Environment.GetEnvironmentVariable("TIMETRACKER_DB_PATH")
?? Path.Combine(builder.Environment.ContentRootPath, "timetracker.db");
builder.Services.AddDbContextFactory<TimetrackerDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"));
builder.Services.AddScoped<TimetrackerService>();
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<TimetrackerDbContext>>();
await using var db = await factory.CreateDbContextAsync();
await db.Database.MigrateAsync();
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
if (app.Configuration.GetValue("EnableHttpsRedirect", !app.Environment.IsDevelopment()))
{
app.UseHttpsRedirection();
}
app.UseAuthentication();
app.UseAuthorization();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
// ── Auth-Endpoints ────────────────────────────────────────────────────────────
app.MapPost("/auth/login", async (HttpContext ctx, AuthService authService) =>
{
var form = await ctx.Request.ReadFormAsync();
var username = form["username"].ToString();
var password = form["password"].ToString();
var user = await authService.LoginAsync(username, password);
if (user == null)
return Results.Redirect("/login?error=invalid");
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity),
new AuthenticationProperties { IsPersistent = true });
return Results.Redirect("/");
}).DisableAntiforgery();
app.MapPost("/auth/register", async (HttpContext ctx, AuthService authService) =>
{
var form = await ctx.Request.ReadFormAsync();
var username = form["username"].ToString();
var password = form["password"].ToString();
var (user, error) = await authService.RegisterAsync(username, password);
if (user == null)
return Results.Redirect($"/login?tab=register&error={Uri.EscapeDataString(error!)}");
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username)
};
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(identity),
new AuthenticationProperties { IsPersistent = true });
return Results.Redirect("/");
}).DisableAntiforgery();
app.MapGet("/auth/logout", async (HttpContext ctx) =>
{
await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.Redirect("/login");
});
app.Run();
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Framework" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Utilities.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Build.Tasks.Core" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-15.1.0.0" newVersion="15.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.IO.Redist" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.1.0.0" newVersion="6.1.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Buffers" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.4.0" newVersion="4.0.4.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.2.0" newVersion="4.0.2.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-6.0.1.0" newVersion="6.0.1.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Threading.Tasks.Extensions" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.2.1.0" newVersion="4.2.1.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
Binary file not shown.
@@ -0,0 +1,171 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v8.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v8.0": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/5.0.0-2.25567.12": {
"dependencies": {
"Microsoft.Build.Locator": "1.10.2",
"Newtonsoft.Json": "13.0.3",
"System.Collections.Immutable": "9.0.0",
"System.CommandLine": "2.0.0-rtm.25509.106"
},
"runtime": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.dll": {}
},
"resources": {
"cs/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "cs"
},
"de/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "de"
},
"es/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "es"
},
"fr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "fr"
},
"it/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "it"
},
"ja/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ja"
},
"ko/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ko"
},
"pl/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "pl"
},
"pt-BR/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "pt-BR"
},
"ru/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "ru"
},
"tr/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "tr"
},
"zh-Hans/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "zh-Hans"
},
"zh-Hant/Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost.resources.dll": {
"locale": "zh-Hant"
}
}
},
"Microsoft.Build.Locator/1.10.2": {
"runtime": {
"lib/net8.0/Microsoft.Build.Locator.dll": {
"assemblyVersion": "1.0.0.0",
"fileVersion": "1.10.2.26959"
}
}
},
"Newtonsoft.Json/13.0.3": {
"runtime": {
"lib/net6.0/Newtonsoft.Json.dll": {
"assemblyVersion": "13.0.0.0",
"fileVersion": "13.0.3.27908"
}
}
},
"System.Collections.Immutable/9.0.0": {
"runtime": {
"lib/net8.0/System.Collections.Immutable.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.24.52809"
}
}
},
"System.CommandLine/2.0.0-rtm.25509.106": {
"runtime": {
"lib/net8.0/System.CommandLine.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.25.51006"
}
},
"resources": {
"lib/net8.0/cs/System.CommandLine.resources.dll": {
"locale": "cs"
},
"lib/net8.0/de/System.CommandLine.resources.dll": {
"locale": "de"
},
"lib/net8.0/es/System.CommandLine.resources.dll": {
"locale": "es"
},
"lib/net8.0/fr/System.CommandLine.resources.dll": {
"locale": "fr"
},
"lib/net8.0/it/System.CommandLine.resources.dll": {
"locale": "it"
},
"lib/net8.0/ja/System.CommandLine.resources.dll": {
"locale": "ja"
},
"lib/net8.0/ko/System.CommandLine.resources.dll": {
"locale": "ko"
},
"lib/net8.0/pl/System.CommandLine.resources.dll": {
"locale": "pl"
},
"lib/net8.0/pt-BR/System.CommandLine.resources.dll": {
"locale": "pt-BR"
},
"lib/net8.0/ru/System.CommandLine.resources.dll": {
"locale": "ru"
},
"lib/net8.0/tr/System.CommandLine.resources.dll": {
"locale": "tr"
},
"lib/net8.0/zh-Hans/System.CommandLine.resources.dll": {
"locale": "zh-Hans"
},
"lib/net8.0/zh-Hant/System.CommandLine.resources.dll": {
"locale": "zh-Hant"
}
}
}
}
},
"libraries": {
"Microsoft.CodeAnalysis.Workspaces.MSBuild.BuildHost/5.0.0-2.25567.12": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Microsoft.Build.Locator/1.10.2": {
"type": "package",
"serviceable": true,
"sha512": "sha512-F+nLS7IpgtslyxNvtD6Jalnf5WU08lu8yfJBNQl3cbEF3AMUphs4t7nPuRYaaU8QZyGrqtVi7i73LhAe/yHx7A==",
"path": "microsoft.build.locator/1.10.2",
"hashPath": "microsoft.build.locator.1.10.2.nupkg.sha512"
},
"Newtonsoft.Json/13.0.3": {
"type": "package",
"serviceable": true,
"sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
"path": "newtonsoft.json/13.0.3",
"hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
},
"System.Collections.Immutable/9.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==",
"path": "system.collections.immutable/9.0.0",
"hashPath": "system.collections.immutable.9.0.0.nupkg.sha512"
},
"System.CommandLine/2.0.0-rtm.25509.106": {
"type": "package",
"serviceable": true,
"sha512": "sha512-IdCQOFNHQfK0hu3tzWOHFJLMaiEOR/4OynmOh+IfukrTIsCR4TTDm7lpuXQyMZ0eRfIyUcz06gHGJNlILAq/6A==",
"path": "system.commandline/2.0.0-rtm.25509.106",
"hashPath": "system.commandline.2.0.0-rtm.25509.106.nupkg.sha512"
}
}
}
@@ -0,0 +1,14 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"rollForward": "Major",
"configProperties": {
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More