Compare commits
7 Commits
main
..
73599c00d1
| Author | SHA1 | Date | |
|---|---|---|---|
| 73599c00d1 | |||
| 64c5f6aa2c | |||
| 5431618d43 | |||
| 598243ddc9 | |||
| 9fd50f86c0 | |||
| 0467f45036 | |||
| 88ac175190 |
-11
@@ -1,11 +0,0 @@
|
|||||||
# SQLite Datenbanken und Journal-Dateien
|
|
||||||
*.db
|
|
||||||
*.db-shm
|
|
||||||
*.db-wal
|
|
||||||
|
|
||||||
# Build-Artefakte
|
|
||||||
bin/
|
|
||||||
obj/
|
|
||||||
*.user
|
|
||||||
*.suo
|
|
||||||
*.vs/
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 MiB |
@@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
<ResourcePreloader />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||||
|
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||||
|
<link rel="stylesheet" href="@Assets["app.css"]" />
|
||||||
|
<link rel="stylesheet" href="@Assets["timetracker.styles.css"]" />
|
||||||
|
<ImportMap />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||||
|
<link rel="alternate icon" type="image/png" href="favicon.png" />
|
||||||
|
<HeadOutlet />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes />
|
||||||
|
<ReconnectModal />
|
||||||
|
<script src="@Assets["_framework/blazor.web.js"]"></script>
|
||||||
|
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<MudThemeProvider Theme="_theme" />
|
||||||
|
<MudPopoverProvider />
|
||||||
|
<MudDialogProvider />
|
||||||
|
<MudSnackbarProvider />
|
||||||
|
|
||||||
|
<MudLayout>
|
||||||
|
<MudAppBar Elevation="2" Style="background: linear-gradient(90deg, #3F51B5 0%, #1A237E 100%);">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="ml-2">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Style="color:white; font-size:1.6rem" />
|
||||||
|
<MudText Typo="Typo.h6" Style="color:white; font-weight:700; letter-spacing:0.5px">Timetracker</MudText>
|
||||||
|
</MudStack>
|
||||||
|
<MudSpacer />
|
||||||
|
</MudAppBar>
|
||||||
|
|
||||||
|
<MudDrawer @bind-Open="_drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
|
||||||
|
<NavMenu />
|
||||||
|
</MudDrawer>
|
||||||
|
|
||||||
|
<MudMainContent>
|
||||||
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4 pb-8">
|
||||||
|
@Body
|
||||||
|
</MudContainer>
|
||||||
|
</MudMainContent>
|
||||||
|
</MudLayout>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private bool _drawerOpen = true;
|
||||||
|
|
||||||
|
private readonly MudTheme _theme = new()
|
||||||
|
{
|
||||||
|
PaletteLight = new PaletteLight
|
||||||
|
{
|
||||||
|
Primary = "#3F51B5",
|
||||||
|
PrimaryDarken = "#1A237E",
|
||||||
|
PrimaryLighten = "#7986CB",
|
||||||
|
Secondary = "#009688",
|
||||||
|
SecondaryDarken = "#00695C",
|
||||||
|
AppbarBackground = "#3F51B5",
|
||||||
|
Background = "#F4F6F9",
|
||||||
|
DrawerBackground = "#FFFFFF",
|
||||||
|
Surface = "#FFFFFF",
|
||||||
|
TextPrimary = "#212121",
|
||||||
|
TextSecondary = "#757575",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private void ToggleDrawer() => _drawerOpen = !_drawerOpen;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
border-bottom: 1px solid #d6d5d5;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a:first-child {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640.98px) {
|
||||||
|
.top-row {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.page {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
height: 100vh;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row.auth ::deep a:first-child {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row, article {
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
padding-right: 1.5rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
color-scheme: light only;
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
||||||
|
</MudNavMenu>
|
||||||
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
.navbar-toggler {
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked {
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-row {
|
||||||
|
min-height: 3.5rem;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
top: -1px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-house-door-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-plus-square-fill-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bi-list-nested-nav-menu {
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:first-of-type {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:last-of-type {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link {
|
||||||
|
color: #d7d7d7;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 3rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep a.active {
|
||||||
|
background-color: rgba(255,255,255,0.37);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item ::deep .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-toggler:checked ~ .nav-scrollable {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
.navbar-toggler {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-scrollable {
|
||||||
|
/* Never collapse the sidebar for wide screens */
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
/* Allow sidebar to scroll for tall menus */
|
||||||
|
height: calc(100vh - 3.5rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-53
@@ -1,9 +1,6 @@
|
|||||||
@page "/feiertage"
|
@page "/feiertage"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize]
|
@inject HolidayService HolidayService
|
||||||
@inject IHolidayService HolidayService
|
|
||||||
@inject ITimetrackerService TrackerService
|
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
|
||||||
|
|
||||||
<PageTitle>Feiertage – Timetracker</PageTitle>
|
<PageTitle>Feiertage – Timetracker</PageTitle>
|
||||||
|
|
||||||
@@ -20,7 +17,7 @@ else
|
|||||||
|
|
||||||
@* ── Header ── *@
|
@* ── Header ── *@
|
||||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||||
Style="background: #0F766E; color: white;">
|
Style="background: linear-gradient(135deg, #00897B 0%, #004D40 100%); color: white;">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||||
Style="color:white" Size="Size.Large" OnClick="PrevYear" />
|
Style="color:white" Size="Size.Large" OnClick="PrevYear" />
|
||||||
@@ -140,7 +137,7 @@ else
|
|||||||
|
|
||||||
@* ── Zusammenfassung ── *@
|
@* ── Zusammenfassung ── *@
|
||||||
<MudPaper Elevation="2" Class="pa-4 rounded-xl"
|
<MudPaper Elevation="2" Class="pa-4 rounded-xl"
|
||||||
Style="background: linear-gradient(90deg, rgba(15,118,110,0.08) 0%, transparent 100%); border-left: 4px solid #0F766E;">
|
Style="background: linear-gradient(90deg, rgba(0,137,123,0.08) 0%, transparent 100%); border-left: 4px solid #00897B;">
|
||||||
<MudStack Row="true" Spacing="4" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center">
|
<MudStack Row="true" Spacing="4" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Celebration" Style="color:#00897B" />
|
<MudIcon Icon="@Icons.Material.Filled.Celebration" Style="color:#00897B" />
|
||||||
@@ -183,65 +180,20 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public AppSettings? PersistentSettings { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<PublicHoliday>? PersistentHolidays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentSubLabel { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentYear { get; set; }
|
|
||||||
|
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private int _year = DateTime.Today.Year;
|
private int _year = DateTime.Today.Year;
|
||||||
private List<PublicHoliday> _holidays = [];
|
private List<PublicHoliday> _holidays = [];
|
||||||
private string _subLabel = "";
|
private string _subLabel = "";
|
||||||
|
|
||||||
private int _userId;
|
|
||||||
private AppSettings _settings = new();
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
|
||||||
if (PersistentSettings != null && PersistentHolidays != null)
|
|
||||||
{
|
|
||||||
_settings = PersistentSettings;
|
|
||||||
_holidays = PersistentHolidays;
|
|
||||||
_subLabel = PersistentSubLabel ?? "";
|
|
||||||
_year = PersistentYear ?? DateTime.Today.Year;
|
|
||||||
_loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
|
||||||
var claim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
|
|
||||||
if (claim != null)
|
|
||||||
{
|
|
||||||
_userId = int.Parse(claim.Value);
|
|
||||||
|
|
||||||
if (PersistentSettings == null || PersistentHolidays == null)
|
|
||||||
{
|
|
||||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
|
||||||
await LoadHolidays();
|
|
||||||
|
|
||||||
PersistentSettings = _settings;
|
|
||||||
PersistentHolidays = _holidays;
|
|
||||||
PersistentSubLabel = _subLabel;
|
|
||||||
PersistentYear = _year;
|
|
||||||
_loading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
await LoadHolidays();
|
await LoadHolidays();
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadHolidays()
|
private async Task LoadHolidays()
|
||||||
{
|
{
|
||||||
_holidays = await HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
|
_holidays = await HolidayService.GetHolidaysAsync(_year);
|
||||||
var count = _holidays.Count;
|
var count = _holidays.Count;
|
||||||
var past = _holidays.Count(h => h.Date < DateOnly.FromDateTime(DateTime.Today));
|
var past = _holidays.Count(h => h.Date < DateOnly.FromDateTime(DateTime.Today));
|
||||||
_subLabel = count == 0
|
_subLabel = count == 0
|
||||||
@@ -1,12 +1,8 @@
|
|||||||
@page "/week"
|
@page "/"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize]
|
@inject TimetrackerService TrackerService
|
||||||
@inject ITimetrackerService TrackerService
|
@inject HolidayService HolidayService
|
||||||
@inject IHolidayService HolidayService
|
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject NavigationManager NavigationManager
|
|
||||||
|
|
||||||
<PageTitle>KW @_kw – Wochenübersicht – Timetracker</PageTitle>
|
<PageTitle>KW @_kw – Wochenübersicht – Timetracker</PageTitle>
|
||||||
|
|
||||||
@@ -22,32 +18,29 @@ else
|
|||||||
<MudStack Spacing="3">
|
<MudStack Spacing="3">
|
||||||
|
|
||||||
@* ── Wochen-Header ── *@
|
@* ── Wochen-Header ── *@
|
||||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl onboarding-week-header mud-theme-primary">
|
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||||
|
Style="background: linear-gradient(135deg, #3F51B5 0%, #1A237E 100%); color: white;">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||||
Style="color: var(--mud-palette-primary-text)" Size="Size.Large" OnClick="PrevWeek" />
|
Style="color:white" Size="Size.Large" OnClick="PrevWeek" />
|
||||||
<MudStack Spacing="0" AlignItems="AlignItems.Center">
|
<MudStack Spacing="0" AlignItems="AlignItems.Center">
|
||||||
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-primary-text); font-weight:700; letter-spacing:0.5px">
|
<MudText Typo="Typo.h5" Style="color:white; font-weight:700; letter-spacing:0.5px">
|
||||||
@_weekLabel
|
@_weekLabel
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudText Typo="Typo.caption" Style="color: var(--mud-palette-primary-text); opacity: 0.72">
|
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,0.72)">
|
||||||
@_weekSubLabel
|
@_weekSubLabel
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
||||||
<MudTooltip Text="Woche als Excel exportieren">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Download"
|
|
||||||
Style="color: var(--mud-palette-primary-text)" OnClick="ExportWeekExcel" />
|
|
||||||
</MudTooltip>
|
|
||||||
@if (!IsCurrentWeek)
|
@if (!IsCurrentWeek)
|
||||||
{
|
{
|
||||||
<MudButton Variant="Variant.Text" Style="color: var(--mud-palette-primary-text)"
|
<MudButton Variant="Variant.Text" Style="color:white"
|
||||||
OnClick="GoToCurrentWeek" Size="Size.Small">
|
OnClick="GoToCurrentWeek" Size="Size.Small">
|
||||||
Heute
|
Heute
|
||||||
</MudButton>
|
</MudButton>
|
||||||
}
|
}
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
|
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
|
||||||
Style="color: var(--mud-palette-primary-text)" Size="Size.Large" OnClick="NextWeek" />
|
Style="color:white" Size="Size.Large" OnClick="NextWeek" />
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -98,9 +91,9 @@ else
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
@* ── Arbeitstag: vollständige Karte ── *@
|
@* ── Arbeitstag: vollständige Karte ── *@
|
||||||
<MudCard @key="@day.Date" Elevation="@(isToday ? 6 : 2)" Class="rounded-xl onboarding-day-card"
|
<MudCard @key="@day.Date" Elevation="@(isToday ? 6 : 2)" Class="rounded-xl"
|
||||||
Style="@($"border-left: 4px solid {borderColor};")">
|
Style="@($"border-left: 4px solid {borderColor};")">
|
||||||
<MudCardHeader Style="@(isToday ? "background: linear-gradient(90deg, rgba(14,165,233,0.07) 0%, transparent 100%);" : "")">
|
<MudCardHeader Style="@(isToday ? "background: linear-gradient(90deg, rgba(63,81,181,0.07) 0%, transparent 100%);" : "")">
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
@@ -142,31 +135,10 @@ else
|
|||||||
</MudChip>
|
</MudChip>
|
||||||
}
|
}
|
||||||
else if (!hasData)
|
else if (!hasData)
|
||||||
{
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
|
||||||
@if (day.IsVacation)
|
|
||||||
{
|
|
||||||
<MudChip T="string" Color="Color.Info" Variant="Variant.Filled" Size="Size.Small" Style="font-weight:700">
|
|
||||||
URLAUB
|
|
||||||
</MudChip>
|
|
||||||
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Error"
|
|
||||||
OnClick="@(async () => await RemoveVacation(day))"
|
|
||||||
Style="text-transform:none; font-size: 0.75rem;">
|
|
||||||
Urlaub entfernen
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
<MudChip T="string" Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small">
|
<MudChip T="string" Color="Color.Default" Variant="Variant.Outlined" Size="Size.Small">
|
||||||
Nicht erfasst
|
Nicht erfasst
|
||||||
</MudChip>
|
</MudChip>
|
||||||
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Info"
|
|
||||||
OnClick="@(async () => await MarkAsVacation(day))"
|
|
||||||
Style="text-transform:none; font-size: 0.75rem;">
|
|
||||||
Als Urlaub eintragen
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
</MudStack>
|
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
@@ -203,43 +175,19 @@ else
|
|||||||
<MudText Color="Color.Secondary" Class="mb-1">−</MudText>
|
<MudText Color="Color.Secondary" Class="mb-1">−</MudText>
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.overline" Color="Color.Secondary">Pausen</MudText>
|
<MudText Typo="Typo.overline" Color="Color.Secondary">Pausen</MudText>
|
||||||
<MudText Typo="Typo.body1">@FormatTs(GetEffectiveBreakTime(day))</MudText>
|
<MudText Typo="Typo.body1">@FormatTs(day.TotalBreakTime)</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudText Color="Color.Secondary" Class="mb-1">=</MudText>
|
<MudText Color="Color.Secondary" Class="mb-1">=</MudText>
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.overline" Color="Color.Secondary">Netto</MudText>
|
<MudText Typo="Typo.overline" Color="Color.Secondary">Netto</MudText>
|
||||||
<MudText Typo="Typo.body1" Color="Color.Primary" Style="font-weight:700">
|
<MudText Typo="Typo.body1" Color="Color.Primary" Style="font-weight:700">
|
||||||
@FormatTs(GetEffectiveNetWork(day)!.Value)
|
@FormatTs(day.NetWork!.Value)
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudDivider Vertical="true" FlexItem="true" Style="height:36px" />
|
<MudDivider Vertical="true" FlexItem="true" Style="height:36px" />
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.overline" Color="Color.Secondary">Soll</MudText>
|
<MudText Typo="Typo.overline" Color="Color.Secondary">Soll</MudText>
|
||||||
<MudText Typo="Typo.body1">@FormatTs(TimeSpan.FromHours(GetDailyTargetHours(day)))</MudText>
|
<MudText Typo="Typo.body1">@FormatTs(TimeSpan.FromHours(_settings.DailyTargetHours))</MudText>
|
||||||
</MudStack>
|
|
||||||
<MudDivider Vertical="true" FlexItem="true" Style="height:36px" />
|
|
||||||
<MudStack Spacing="0">
|
|
||||||
<MudText Typo="Typo.overline" Color="Color.Secondary">Soll-Ende</MudText>
|
|
||||||
<MudText Typo="Typo.body1">@FormatTimeSpan(CalculateEarliestDeparture(day)) Uhr</MudText>
|
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
|
||||||
</MudItem>
|
|
||||||
}
|
|
||||||
else if (day.Start.HasValue)
|
|
||||||
{
|
|
||||||
<MudItem xs="12" md="8">
|
|
||||||
<MudStack Row="true" Spacing="3" AlignItems="AlignItems.Center" Class="h-100 pl-1">
|
|
||||||
<MudStack Spacing="0">
|
|
||||||
<MudText Typo="Typo.overline" Style="color: var(--mud-palette-info);">Früheste Gehenszeit</MudText>
|
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
|
||||||
<MudIcon Icon="@Icons.Material.Filled.ExitToApp" Color="Color.Info" Size="Size.Small" />
|
|
||||||
<MudText Typo="Typo.body1" Color="Color.Info" Style="font-weight:700">
|
|
||||||
@FormatTimeSpan(CalculateEarliestDeparture(day)) Uhr
|
|
||||||
</MudText>
|
|
||||||
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="ml-2">
|
|
||||||
(Bei Soll: @FormatTs(TimeSpan.FromHours(_settings.DailyTargetHours))h + @((int)GetRequiredBreakForTarget(day).TotalMinutes) Min. Pause)
|
|
||||||
</MudText>
|
|
||||||
</MudStack>
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
@@ -249,7 +197,7 @@ else
|
|||||||
@* ── Fortschrittsbalken ── *@
|
@* ── Fortschrittsbalken ── *@
|
||||||
@if (progressPct >= 0)
|
@if (progressPct >= 0)
|
||||||
{
|
{
|
||||||
<MudTooltip Text="@($"Netto {FormatTs(GetEffectiveNetWork(day) ?? TimeSpan.Zero)} von {FormatTs(TimeSpan.FromHours(GetDailyTargetHours(day)))} Soll")">
|
<MudTooltip Text="@($"Netto {FormatTs(day.NetWork ?? TimeSpan.Zero)} von {FormatTs(TimeSpan.FromHours(_settings.DailyTargetHours))} Soll")">
|
||||||
<MudProgressLinear Value="@Math.Min(progressPct, 100)"
|
<MudProgressLinear Value="@Math.Min(progressPct, 100)"
|
||||||
Color="@(progressPct >= 100 ? Color.Success : Color.Primary)"
|
Color="@(progressPct >= 100 ? Color.Success : Color.Primary)"
|
||||||
Rounded="true" Class="mt-3" Style="height:6px" />
|
Rounded="true" Class="mt-3" Style="height:6px" />
|
||||||
@@ -319,35 +267,35 @@ else
|
|||||||
}
|
}
|
||||||
|
|
||||||
@* ── Wochensumme ── *@
|
@* ── Wochensumme ── *@
|
||||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl onboarding-week-summary"
|
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||||
Style="background: var(--mud-palette-primary-darken); color: var(--mud-palette-primary-text);">
|
Style="background: linear-gradient(135deg, #1A237E 0%, #283593 100%); color:white;">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-4">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-4">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Summarize" Style="color: var(--mud-palette-primary-text); opacity: 0.8" />
|
<MudIcon Icon="@Icons.Material.Filled.Summarize" Style="color:rgba(255,255,255,0.8)" />
|
||||||
<MudText Typo="Typo.h6" Style="color: var(--mud-palette-primary-text); font-weight:600">Wochensumme</MudText>
|
<MudText Typo="Typo.h6" Style="color:white; font-weight:600">Wochensumme</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudGrid Spacing="3">
|
<MudGrid Spacing="3">
|
||||||
<MudItem xs="6" sm="3">
|
<MudItem xs="6" sm="3">
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.overline" Style="color: var(--mud-palette-primary-text); opacity: 0.6">Brutto</MudText>
|
<MudText Typo="Typo.overline" Style="color:rgba(255,255,255,0.6)">Brutto</MudText>
|
||||||
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-primary-text)">@FormatTs(WeekGross)</MudText>
|
<MudText Typo="Typo.h5" Style="color:white">@FormatTs(WeekGross)</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="6" sm="3">
|
<MudItem xs="6" sm="3">
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.overline" Style="color: var(--mud-palette-primary-text); opacity: 0.6">Pausen</MudText>
|
<MudText Typo="Typo.overline" Style="color:rgba(255,255,255,0.6)">Pausen</MudText>
|
||||||
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-primary-text)">@FormatTs(WeekBreaks)</MudText>
|
<MudText Typo="Typo.h5" Style="color:white">@FormatTs(WeekBreaks)</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="6" sm="3">
|
<MudItem xs="6" sm="3">
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.overline" Style="color: var(--mud-palette-primary-text); opacity: 0.6">Netto</MudText>
|
<MudText Typo="Typo.overline" Style="color:rgba(255,255,255,0.6)">Netto</MudText>
|
||||||
<MudText Typo="Typo.h5" Color="Color.Secondary" Style="font-weight:700">@FormatTs(WeekNet)</MudText>
|
<MudText Typo="Typo.h5" Style="color:#90CAF9; font-weight:700">@FormatTs(WeekNet)</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="6" sm="3">
|
<MudItem xs="6" sm="3">
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.overline" Style="color: var(--mud-palette-primary-text); opacity: 0.6">Gleitzeit</MudText>
|
<MudText Typo="Typo.overline" Style="color:rgba(255,255,255,0.6)">Gleitzeit</MudText>
|
||||||
<MudText Typo="Typo.h5" Color="@(WeekOvertime >= TimeSpan.Zero ? Color.Success : Color.Warning)" Style="font-weight:700">
|
<MudText Typo="Typo.h5" Style="@($"color:{(WeekOvertime >= TimeSpan.Zero ? "#A5D6A7" : "#FFCC80")}; font-weight:700")">
|
||||||
@FormatTs(WeekOvertime, sign: true)
|
@FormatTs(WeekOvertime, sign: true)
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -356,7 +304,7 @@ else
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
@* ── Gleitzeitkonto ── *@
|
@* ── Gleitzeitkonto ── *@
|
||||||
<MudPaper Elevation="3" Class="pa-5 rounded-xl onboarding-overtime-balance"
|
<MudPaper Elevation="3" Class="pa-5 rounded-xl"
|
||||||
Style="@($"border-left: 6px solid {(_totalOvertime >= TimeSpan.Zero ? "#4CAF50" : "#FF9800")};")">
|
Style="@($"border-left: 6px solid {(_totalOvertime >= TimeSpan.Zero ? "#4CAF50" : "#FF9800")};")">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap" Spacing="3">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap" Spacing="3">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||||
@@ -378,42 +326,12 @@ else
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
@if (_showOnboarding)
|
|
||||||
{
|
|
||||||
<OnboardingTour OnFinished="HandleOnboardingFinished" />
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public AppSettings? PersistentSettings { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<DayVm>? PersistentDays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public TimeSpan? PersistentTotalOvertime { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public Dictionary<DateOnly, string>? PersistentHolidays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentHolidayYear { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<VacationDay>? PersistentVacations { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentWeekLabel { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentWeekSubLabel { get; set; }
|
|
||||||
|
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private int _userId;
|
|
||||||
private DateOnly _monday;
|
private DateOnly _monday;
|
||||||
private List<DayVm> _days = [];
|
private List<DayVm> _days = [];
|
||||||
private AppSettings _settings = new();
|
private AppSettings _settings = new();
|
||||||
@@ -426,102 +344,31 @@ else
|
|||||||
private TimeSpan _totalOvertime;
|
private TimeSpan _totalOvertime;
|
||||||
private Dictionary<DateOnly, string> _holidays = [];
|
private Dictionary<DateOnly, string> _holidays = [];
|
||||||
private int _holidayYear = -1;
|
private int _holidayYear = -1;
|
||||||
private bool _showOnboarding;
|
|
||||||
private List<VacationDay> _vacations = [];
|
|
||||||
|
|
||||||
private bool IsCurrentWeek => _monday == GetMonday(DateOnly.FromDateTime(DateTime.Today));
|
private bool IsCurrentWeek => _monday == GetMonday(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (PersistentSettings != null && PersistentDays != null && PersistentTotalOvertime != null)
|
|
||||||
{
|
|
||||||
_settings = PersistentSettings;
|
|
||||||
_days = PersistentDays;
|
|
||||||
_totalOvertime = PersistentTotalOvertime.Value;
|
|
||||||
_holidays = PersistentHolidays ?? [];
|
|
||||||
_holidayYear = PersistentHolidayYear ?? -1;
|
|
||||||
_vacations = PersistentVacations ?? [];
|
|
||||||
_weekLabel = PersistentWeekLabel ?? "";
|
|
||||||
_weekSubLabel = PersistentWeekSubLabel ?? "";
|
|
||||||
_loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
|
||||||
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
|
|
||||||
if (claim == null) return; // Prerender-Pass – Circuit noch nicht authentifiziert
|
|
||||||
_userId = int.Parse(claim.Value);
|
|
||||||
_monday = GetMonday(DateOnly.FromDateTime(DateTime.Today));
|
_monday = GetMonday(DateOnly.FromDateTime(DateTime.Today));
|
||||||
|
_settings = await TrackerService.GetSettingsAsync();
|
||||||
if (PersistentSettings == null || PersistentDays == null || PersistentTotalOvertime == null)
|
await LoadWeek();
|
||||||
{
|
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_settings);
|
||||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
|
||||||
|
|
||||||
var loadWeekTask = LoadWeek();
|
|
||||||
var overtimeTask = TrackerService.GetTotalOvertimeAsync(_userId, _settings);
|
|
||||||
|
|
||||||
await Task.WhenAll(loadWeekTask, overtimeTask);
|
|
||||||
_totalOvertime = await overtimeTask;
|
|
||||||
|
|
||||||
PersistentSettings = _settings;
|
|
||||||
PersistentDays = _days;
|
|
||||||
PersistentTotalOvertime = _totalOvertime;
|
|
||||||
PersistentHolidays = _holidays;
|
|
||||||
PersistentHolidayYear = _holidayYear;
|
|
||||||
PersistentVacations = _vacations;
|
|
||||||
PersistentWeekLabel = _weekLabel;
|
|
||||||
PersistentWeekSubLabel = _weekSubLabel;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var showOnb = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "showOnboarding");
|
|
||||||
_showOnboarding = showOnb == "true";
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Ignored during prerendering/SSR
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleOnboardingFinished()
|
|
||||||
{
|
|
||||||
_showOnboarding = false;
|
|
||||||
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "showOnboarding", "false");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadWeek()
|
private async Task LoadWeek()
|
||||||
{
|
{
|
||||||
Task<List<PublicHoliday>> holidaysTask;
|
|
||||||
if (_monday.Year != _holidayYear)
|
if (_monday.Year != _holidayYear)
|
||||||
{
|
{
|
||||||
holidaysTask = HolidayService.GetHolidaysAsync(_monday.Year, _settings.GermanState);
|
var list = await HolidayService.GetHolidaysAsync(_monday.Year);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
holidaysTask = Task.FromResult(new List<PublicHoliday>());
|
|
||||||
}
|
|
||||||
|
|
||||||
var dbDaysTask = TrackerService.GetWeekAsync(_userId, _monday);
|
|
||||||
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _monday.Year);
|
|
||||||
|
|
||||||
await Task.WhenAll(holidaysTask, dbDaysTask, vacationsTask);
|
|
||||||
|
|
||||||
if (_monday.Year != _holidayYear)
|
|
||||||
{
|
|
||||||
var list = await holidaysTask;
|
|
||||||
_holidays = list.ToDictionary(h => h.Date, h => h.Name);
|
_holidays = list.ToDictionary(h => h.Date, h => h.Name);
|
||||||
_holidayYear = _monday.Year;
|
_holidayYear = _monday.Year;
|
||||||
}
|
}
|
||||||
|
var dbDays = await TrackerService.GetWeekAsync(_monday);
|
||||||
_vacations = await vacationsTask;
|
|
||||||
var dbDays = await dbDaysTask;
|
|
||||||
_days = Enumerable.Range(0, 7).Select(i =>
|
_days = Enumerable.Range(0, 7).Select(i =>
|
||||||
{
|
{
|
||||||
var date = _monday.AddDays(i);
|
var date = _monday.AddDays(i);
|
||||||
var dayVm = DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date, _userId);
|
return DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date);
|
||||||
dayVm.IsVacation = _vacations.Any(v => v.Date == date);
|
|
||||||
return dayVm;
|
|
||||||
}).ToList();
|
}).ToList();
|
||||||
BuildWeekLabels();
|
BuildWeekLabels();
|
||||||
}
|
}
|
||||||
@@ -545,10 +392,6 @@ else
|
|||||||
private async Task PrevWeek() { _monday = _monday.AddDays(-7); await LoadWeek(); }
|
private async Task PrevWeek() { _monday = _monday.AddDays(-7); await LoadWeek(); }
|
||||||
private async Task NextWeek() { _monday = _monday.AddDays(7); await LoadWeek(); }
|
private async Task NextWeek() { _monday = _monday.AddDays(7); await LoadWeek(); }
|
||||||
private async Task GoToCurrentWeek() { _monday = GetMonday(DateOnly.FromDateTime(DateTime.Today)); await LoadWeek(); }
|
private async Task GoToCurrentWeek() { _monday = GetMonday(DateOnly.FromDateTime(DateTime.Today)); await LoadWeek(); }
|
||||||
private void ExportWeekExcel()
|
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo($"api/tracker/export/week?monday={_monday:yyyy-MM-dd}", forceLoad: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DateOnly GetMonday(DateOnly date)
|
private static DateOnly GetMonday(DateOnly date)
|
||||||
{
|
{
|
||||||
@@ -578,66 +421,26 @@ else
|
|||||||
private async Task SaveDay(DayVm day)
|
private async Task SaveDay(DayVm day)
|
||||||
{
|
{
|
||||||
await TrackerService.UpsertWorkDayAsync(day.ToWorkDay());
|
await TrackerService.UpsertWorkDayAsync(day.ToWorkDay());
|
||||||
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
|
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_settings);
|
||||||
BuildWeekLabels();
|
BuildWeekLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Berechnungen ─────────────────────────────────────────────
|
// ── Berechnungen ─────────────────────────────────────────────
|
||||||
private TimeSpan GetEffectiveBreakTime(DayVm day)
|
private TimeSpan? GetOvertime(DayVm day) =>
|
||||||
{
|
day.NetWork.HasValue ? day.NetWork.Value - TimeSpan.FromHours(_settings.DailyTargetHours) : null;
|
||||||
var enteredBreak = day.Breaks
|
|
||||||
.Where(b => b.Duration.HasValue)
|
|
||||||
.Aggregate(TimeSpan.Zero, (s, b) => s + b.Duration!.Value);
|
|
||||||
|
|
||||||
if (!day.Start.HasValue || !day.End.HasValue)
|
|
||||||
return enteredBreak;
|
|
||||||
|
|
||||||
var gross = day.End.Value - day.Start.Value;
|
|
||||||
int minMinutes = gross.TotalHours >= 9.0 ? 45 : 30;
|
|
||||||
int finalMin = Math.Max(minMinutes, _settings.MinimumBreakMinutes);
|
|
||||||
var minBreak = TimeSpan.FromMinutes(finalMin);
|
|
||||||
|
|
||||||
return enteredBreak > minBreak ? enteredBreak : minBreak;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TimeSpan? GetEffectiveNetWork(DayVm day)
|
|
||||||
{
|
|
||||||
if (!day.GrossWork.HasValue) return null;
|
|
||||||
return day.GrossWork.Value - GetEffectiveBreakTime(day);
|
|
||||||
}
|
|
||||||
|
|
||||||
private double GetDailyTargetHours(DayVm day)
|
|
||||||
{
|
|
||||||
var isHoliday = _holidays.ContainsKey(day.Date);
|
|
||||||
var isWorkDay = _settings.IsWorkDay(day.Date.DayOfWeek);
|
|
||||||
if (!isWorkDay || isHoliday || day.IsVacation)
|
|
||||||
{
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
return _settings.DailyTargetHours;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TimeSpan? GetOvertime(DayVm day)
|
|
||||||
{
|
|
||||||
var net = GetEffectiveNetWork(day);
|
|
||||||
return net.HasValue ? net.Value - TimeSpan.FromHours(GetDailyTargetHours(day)) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetProgressPercent(DayVm day)
|
private int GetProgressPercent(DayVm day)
|
||||||
{
|
{
|
||||||
var net = GetEffectiveNetWork(day);
|
if (!day.NetWork.HasValue || _settings.DailyTargetHours <= 0) return -1;
|
||||||
var target = GetDailyTargetHours(day);
|
return (int)(day.NetWork.Value.TotalHours / _settings.DailyTargetHours * 100);
|
||||||
if (!net.HasValue || target <= 0) return -1;
|
|
||||||
return (int)(net.Value.TotalHours / target * 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetBorderColor(DayVm day, bool isWorkDay, List<string> errors)
|
private static string GetBorderColor(DayVm day, bool isWorkDay, List<string> errors)
|
||||||
{
|
{
|
||||||
if (!isWorkDay) return "#CFD8DC";
|
if (!isWorkDay) return "#CFD8DC";
|
||||||
if (errors.Count > 0) return "#EF5350";
|
if (errors.Count > 0) return "#EF5350";
|
||||||
if (day.IsVacation) return "#0EA5E9"; // Cool blue for vacation
|
|
||||||
if (!day.Start.HasValue && !day.End.HasValue) return "#B0BEC5";
|
if (!day.Start.HasValue && !day.End.HasValue) return "#B0BEC5";
|
||||||
if (!GetEffectiveNetWork(day).HasValue) return "#B0BEC5";
|
if (!day.NetWork.HasValue) return "#B0BEC5";
|
||||||
return "#4CAF50";
|
return "#4CAF50";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,17 +467,11 @@ else
|
|||||||
yield return $"Pause {i + 1} und Pause {j + 1} überschneiden sich.";
|
yield return $"Pause {i + 1} und Pause {j + 1} überschneiden sich.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var net = GetEffectiveNetWork(day);
|
|
||||||
if (net.HasValue && net.Value.TotalHours > 10.0)
|
|
||||||
{
|
|
||||||
yield return "Die tägliche Arbeitszeit überschreitet das gesetzliche Maximum von 10 Stunden!";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimeSpan WeekGross => _days.Aggregate(TimeSpan.Zero, (s, d) => s + (d.GrossWork ?? TimeSpan.Zero));
|
private TimeSpan WeekGross => _days.Aggregate(TimeSpan.Zero, (s, d) => s + (d.GrossWork ?? TimeSpan.Zero));
|
||||||
private TimeSpan WeekBreaks => _days.Aggregate(TimeSpan.Zero, (s, d) => s + GetEffectiveBreakTime(d));
|
private TimeSpan WeekBreaks => _days.Aggregate(TimeSpan.Zero, (s, d) => s + d.TotalBreakTime);
|
||||||
private TimeSpan WeekNet => _days.Aggregate(TimeSpan.Zero, (s, d) => s + (GetEffectiveNetWork(d) ?? TimeSpan.Zero));
|
private TimeSpan WeekNet => _days.Aggregate(TimeSpan.Zero, (s, d) => s + (d.NetWork ?? TimeSpan.Zero));
|
||||||
private TimeSpan WeekOvertime => _days.Aggregate(TimeSpan.Zero, (s, d) => s + (GetOvertime(d) ?? TimeSpan.Zero));
|
private TimeSpan WeekOvertime => _days.Aggregate(TimeSpan.Zero, (s, d) => s + (GetOvertime(d) ?? TimeSpan.Zero));
|
||||||
|
|
||||||
private static string FormatTs(TimeSpan ts, bool sign = false)
|
private static string FormatTs(TimeSpan ts, bool sign = false)
|
||||||
@@ -685,72 +482,14 @@ else
|
|||||||
return $"{prefix}{(int)abs.TotalHours}:{abs.Minutes:D2}";
|
return $"{prefix}{(int)abs.TotalHours}:{abs.Minutes:D2}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private TimeSpan GetRequiredBreakForTarget(DayVm day)
|
|
||||||
{
|
|
||||||
var targetNet = _settings.DailyTargetHours;
|
|
||||||
var minMinutes = targetNet >= 8.5 ? 45 : 30;
|
|
||||||
var requiredBreak = TimeSpan.FromMinutes(Math.Max(minMinutes, _settings.MinimumBreakMinutes));
|
|
||||||
|
|
||||||
var enteredBreak = day.Breaks
|
|
||||||
.Where(b => b.Duration.HasValue)
|
|
||||||
.Aggregate(TimeSpan.Zero, (s, b) => s + b.Duration!.Value);
|
|
||||||
|
|
||||||
return enteredBreak > requiredBreak ? enteredBreak : requiredBreak;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TimeSpan CalculateEarliestDeparture(DayVm day)
|
|
||||||
{
|
|
||||||
if (!day.Start.HasValue) return TimeSpan.Zero;
|
|
||||||
var target = TimeSpan.FromHours(_settings.DailyTargetHours);
|
|
||||||
var finalBreak = GetRequiredBreakForTarget(day);
|
|
||||||
return day.Start.Value + target + finalBreak;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task MarkAsVacation(DayVm day)
|
|
||||||
{
|
|
||||||
var vacation = new VacationDay
|
|
||||||
{
|
|
||||||
UserId = _userId,
|
|
||||||
Date = day.Date,
|
|
||||||
Note = "Über Wochenübersicht eingetragen"
|
|
||||||
};
|
|
||||||
await TrackerService.AddVacationDayAsync(vacation);
|
|
||||||
|
|
||||||
await LoadWeek();
|
|
||||||
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
|
|
||||||
Snackbar.Add($"{day.Date:dd.MM.yyyy} als Urlaub eingetragen.", Severity.Success);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveVacation(DayVm day)
|
|
||||||
{
|
|
||||||
var item = _vacations.FirstOrDefault(v => v.Date == day.Date);
|
|
||||||
if (item != null)
|
|
||||||
{
|
|
||||||
await TrackerService.RemoveVacationDayAsync(_userId, item.Id);
|
|
||||||
|
|
||||||
await LoadWeek();
|
|
||||||
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
|
|
||||||
Snackbar.Add($"Urlaub für {day.Date:dd.MM.yyyy} entfernt.", Severity.Info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatTimeSpan(TimeSpan? ts)
|
|
||||||
{
|
|
||||||
if (!ts.HasValue) return "";
|
|
||||||
var hours = ((int)ts.Value.TotalHours) % 24;
|
|
||||||
return $"{hours:D2}:{ts.Value.Minutes:D2}";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── ViewModels ────────────────────────────────────────────────
|
// ── ViewModels ────────────────────────────────────────────────
|
||||||
public sealed class DayVm
|
private sealed class DayVm
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int UserId { get; set; }
|
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
public TimeSpan? Start { get; set; }
|
public TimeSpan? Start { get; set; }
|
||||||
public TimeSpan? End { get; set; }
|
public TimeSpan? End { get; set; }
|
||||||
public List<BreakVm> Breaks { get; set; } = [];
|
public List<BreakVm> Breaks { get; set; } = [];
|
||||||
public bool IsVacation { get; set; }
|
|
||||||
|
|
||||||
public TimeSpan? GrossWork =>
|
public TimeSpan? GrossWork =>
|
||||||
Start.HasValue && End.HasValue && End > Start ? End.Value - Start.Value : null;
|
Start.HasValue && End.HasValue && End > Start ? End.Value - Start.Value : null;
|
||||||
@@ -761,10 +500,9 @@ else
|
|||||||
|
|
||||||
public TimeSpan? NetWork => GrossWork.HasValue ? GrossWork.Value - TotalBreakTime : null;
|
public TimeSpan? NetWork => GrossWork.HasValue ? GrossWork.Value - TotalBreakTime : null;
|
||||||
|
|
||||||
public static DayVm From(WorkDay? wd, DateOnly date, int userId) => new()
|
public static DayVm From(WorkDay? wd, DateOnly date) => new()
|
||||||
{
|
{
|
||||||
Id = wd?.Id ?? 0,
|
Id = wd?.Id ?? 0,
|
||||||
UserId = wd?.UserId ?? userId,
|
|
||||||
Date = date,
|
Date = date,
|
||||||
Start = wd?.StartTime?.ToTimeSpan(),
|
Start = wd?.StartTime?.ToTimeSpan(),
|
||||||
End = wd?.EndTime?.ToTimeSpan(),
|
End = wd?.EndTime?.ToTimeSpan(),
|
||||||
@@ -779,7 +517,6 @@ else
|
|||||||
public WorkDay ToWorkDay() => new()
|
public WorkDay ToWorkDay() => new()
|
||||||
{
|
{
|
||||||
Id = Id,
|
Id = Id,
|
||||||
UserId = UserId,
|
|
||||||
Date = Date,
|
Date = Date,
|
||||||
StartTime = Start.HasValue ? TimeOnly.FromTimeSpan(Start.Value) : null,
|
StartTime = Start.HasValue ? TimeOnly.FromTimeSpan(Start.Value) : null,
|
||||||
EndTime = End.HasValue ? TimeOnly.FromTimeSpan(End.Value) : null,
|
EndTime = End.HasValue ? TimeOnly.FromTimeSpan(End.Value) : null,
|
||||||
@@ -792,7 +529,7 @@ else
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class BreakVm
|
private sealed class BreakVm
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public TimeSpan? Start { get; set; }
|
public TimeSpan? Start { get; set; }
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
@page "/month"
|
@page "/month"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize]
|
@inject TimetrackerService TrackerService
|
||||||
@inject ITimetrackerService TrackerService
|
@inject HolidayService HolidayService
|
||||||
@inject IHolidayService HolidayService
|
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
|
||||||
@inject NavigationManager NavigationManager
|
|
||||||
|
|
||||||
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker</PageTitle>
|
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker</PageTitle>
|
||||||
|
|
||||||
@@ -20,36 +17,29 @@ else
|
|||||||
<MudStack Spacing="3">
|
<MudStack Spacing="3">
|
||||||
|
|
||||||
@* ── Monats-Header ── *@
|
@* ── Monats-Header ── *@
|
||||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl mud-theme-primary">
|
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||||
|
Style="background: linear-gradient(135deg, #3F51B5 0%, #1A237E 100%); color: white;">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||||
Style="color: var(--mud-palette-primary-text)" Size="Size.Large" OnClick="PrevMonth" />
|
Style="color:white" Size="Size.Large" OnClick="PrevMonth" />
|
||||||
<MudStack Spacing="0" AlignItems="AlignItems.Center">
|
<MudStack Spacing="0" AlignItems="AlignItems.Center">
|
||||||
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-primary-text); font-weight:700; letter-spacing:0.5px">
|
<MudText Typo="Typo.h5" Style="color:white; font-weight:700; letter-spacing:0.5px">
|
||||||
@_deCulture.DateTimeFormat.GetMonthName(_month) @_year
|
@_deCulture.DateTimeFormat.GetMonthName(_month) @_year
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudText Typo="Typo.caption" Style="color: var(--mud-palette-primary-text); opacity: 0.72">
|
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,0.72)">
|
||||||
@_subLabel
|
@_subLabel
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="0">
|
||||||
<MudTooltip Text="Stundenzettel (PDF) herunterladen">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.PictureAsPdf"
|
|
||||||
Style="color: var(--mud-palette-primary-text)" OnClick="ExportMonthPdf" />
|
|
||||||
</MudTooltip>
|
|
||||||
<MudTooltip Text="Monat als Excel exportieren">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Download"
|
|
||||||
Style="color: var(--mud-palette-primary-text)" OnClick="ExportMonthExcel" />
|
|
||||||
</MudTooltip>
|
|
||||||
@if (!IsCurrentMonth)
|
@if (!IsCurrentMonth)
|
||||||
{
|
{
|
||||||
<MudButton Variant="Variant.Text" Style="color: var(--mud-palette-primary-text)"
|
<MudButton Variant="Variant.Text" Style="color:white"
|
||||||
OnClick="GoToCurrentMonth" Size="Size.Small">
|
OnClick="GoToCurrentMonth" Size="Size.Small">
|
||||||
Heute
|
Heute
|
||||||
</MudButton>
|
</MudButton>
|
||||||
}
|
}
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
|
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
|
||||||
Style="color: var(--mud-palette-primary-text)" Size="Size.Large" OnClick="NextMonth" />
|
Style="color:white" Size="Size.Large" OnClick="NextMonth" />
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -59,7 +49,7 @@ else
|
|||||||
<MudCardContent Class="pa-0">
|
<MudCardContent Class="pa-0">
|
||||||
<MudSimpleTable Dense="true" Striped="false" Hover="true" Style="overflow-x:auto">
|
<MudSimpleTable Dense="true" Striped="false" Hover="true" Style="overflow-x:auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style="background: rgba(14,165,233,0.08);">
|
<tr style="background: rgba(63,81,181,0.08);">
|
||||||
<th style="font-weight:700; padding:10px 16px">Tag</th>
|
<th style="font-weight:700; padding:10px 16px">Tag</th>
|
||||||
<th style="font-weight:700; padding:10px 16px">Start</th>
|
<th style="font-weight:700; padding:10px 16px">Start</th>
|
||||||
<th style="font-weight:700; padding:10px 16px">Ende</th>
|
<th style="font-weight:700; padding:10px 16px">Ende</th>
|
||||||
@@ -103,7 +93,7 @@ else
|
|||||||
|
|
||||||
@* ── Monatszusammenfassung ── *@
|
@* ── Monatszusammenfassung ── *@
|
||||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||||
Style="background: linear-gradient(135deg, color-mix(in srgb, var(--mud-palette-primary) 8%, transparent) 0%, color-mix(in srgb, var(--mud-palette-primary) 2%, transparent) 100%); border-left: 6px solid var(--mud-palette-primary);">
|
Style="background: linear-gradient(135deg, rgba(63,81,181,0.08) 0%, rgba(26,35,126,0.04) 100%); border-left: 6px solid #3F51B5;">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-4">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-4">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.CalendarViewMonth" Color="Color.Primary" />
|
<MudIcon Icon="@Icons.Material.Filled.CalendarViewMonth" Color="Color.Primary" />
|
||||||
<MudText Typo="Typo.h6" Style="font-weight:700">Monatszusammenfassung</MudText>
|
<MudText Typo="Typo.h6" Style="font-weight:700">Monatszusammenfassung</MudText>
|
||||||
@@ -159,35 +149,7 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public AppSettings? PersistentSettings { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<MonthDayVm>? PersistentDays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentSubLabel { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public TimeSpan? PersistentMonthNet { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public TimeSpan? PersistentMonthOvertime { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentRecordedWorkDays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentTotalWorkDays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentVacationCount { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentHolidayCount { get; set; }
|
|
||||||
|
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private int _userId;
|
|
||||||
private int _year = DateTime.Today.Year;
|
private int _year = DateTime.Today.Year;
|
||||||
private int _month = DateTime.Today.Month;
|
private int _month = DateTime.Today.Month;
|
||||||
private List<MonthDayVm> _days = [];
|
private List<MonthDayVm> _days = [];
|
||||||
@@ -205,54 +167,16 @@ else
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (PersistentSettings != null && PersistentDays != null)
|
_settings = await TrackerService.GetSettingsAsync();
|
||||||
{
|
|
||||||
_settings = PersistentSettings;
|
|
||||||
_days = PersistentDays;
|
|
||||||
_subLabel = PersistentSubLabel ?? "";
|
|
||||||
_monthNet = PersistentMonthNet ?? TimeSpan.Zero;
|
|
||||||
_monthOvertime = PersistentMonthOvertime ?? TimeSpan.Zero;
|
|
||||||
_recordedWorkDays = PersistentRecordedWorkDays ?? 0;
|
|
||||||
_totalWorkDays = PersistentTotalWorkDays ?? 0;
|
|
||||||
_vacationCount = PersistentVacationCount ?? 0;
|
|
||||||
_holidayCount = PersistentHolidayCount ?? 0;
|
|
||||||
_loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
|
||||||
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
|
|
||||||
if (claim == null) return;
|
|
||||||
_userId = int.Parse(claim.Value);
|
|
||||||
|
|
||||||
if (PersistentSettings == null || PersistentDays == null)
|
|
||||||
{
|
|
||||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
|
||||||
await LoadMonth();
|
await LoadMonth();
|
||||||
|
|
||||||
PersistentSettings = _settings;
|
|
||||||
PersistentDays = _days;
|
|
||||||
PersistentSubLabel = _subLabel;
|
|
||||||
PersistentMonthNet = _monthNet;
|
|
||||||
PersistentMonthOvertime = _monthOvertime;
|
|
||||||
PersistentRecordedWorkDays = _recordedWorkDays;
|
|
||||||
PersistentTotalWorkDays = _totalWorkDays;
|
|
||||||
PersistentVacationCount = _vacationCount;
|
|
||||||
PersistentHolidayCount = _holidayCount;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadMonth()
|
private async Task LoadMonth()
|
||||||
{
|
{
|
||||||
var workDaysTask = TrackerService.GetMonthAsync(_userId, _year, _month);
|
var workDays = await TrackerService.GetMonthAsync(_year, _month);
|
||||||
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
|
var holidays = await HolidayService.GetHolidaysAsync(_year);
|
||||||
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year);
|
var vacations = await TrackerService.GetVacationDaysAsync(_year);
|
||||||
|
|
||||||
await Task.WhenAll(workDaysTask, holidaysTask, vacationsTask);
|
|
||||||
|
|
||||||
var workDays = await workDaysTask;
|
|
||||||
var holidays = await holidaysTask;
|
|
||||||
var vacations = await vacationsTask;
|
|
||||||
|
|
||||||
var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name);
|
var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name);
|
||||||
var vacationSet = vacations.Select(v => v.Date).ToHashSet();
|
var vacationSet = vacations.Select(v => v.Date).ToHashSet();
|
||||||
@@ -273,10 +197,7 @@ else
|
|||||||
.Where(b => b.StartTime.HasValue && b.EndTime.HasValue && b.EndTime > b.StartTime)
|
.Where(b => b.StartTime.HasValue && b.EndTime.HasValue && b.EndTime > b.StartTime)
|
||||||
.Aggregate(TimeSpan.Zero, (s, b) =>
|
.Aggregate(TimeSpan.Zero, (s, b) =>
|
||||||
s + (b.EndTime!.Value.ToTimeSpan() - b.StartTime!.Value.ToTimeSpan()));
|
s + (b.EndTime!.Value.ToTimeSpan() - b.StartTime!.Value.ToTimeSpan()));
|
||||||
var minMinutes = gross.TotalHours >= 9.0 ? 45 : 30;
|
net = gross - breaks;
|
||||||
var minBreak = TimeSpan.FromMinutes(Math.Max(minMinutes, _settings.MinimumBreakMinutes));
|
|
||||||
var finalBreak = breaks > minBreak ? breaks : minBreak;
|
|
||||||
net = gross - finalBreak;
|
|
||||||
if (net < TimeSpan.Zero) net = null;
|
if (net < TimeSpan.Zero) net = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,16 +253,6 @@ else
|
|||||||
await LoadMonth();
|
await LoadMonth();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExportMonthExcel()
|
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo($"api/tracker/export/month?year={_year}&month={_month}", forceLoad: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ExportMonthPdf()
|
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo($"api/tracker/export/month/pdf?userId={_userId}&year={_year}&month={_month}", forceLoad: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatTs(TimeSpan ts, bool sign = false)
|
private static string FormatTs(TimeSpan ts, bool sign = false)
|
||||||
{
|
{
|
||||||
var neg = ts < TimeSpan.Zero;
|
var neg = ts < TimeSpan.Zero;
|
||||||
@@ -381,7 +292,7 @@ else
|
|||||||
else Chip("Ausstehend", "#CFD8DC");
|
else Chip("Ausstehend", "#CFD8DC");
|
||||||
};
|
};
|
||||||
|
|
||||||
public sealed class MonthDayVm
|
private sealed class MonthDayVm
|
||||||
{
|
{
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
public TimeOnly? StartTime { get; set; }
|
public TimeOnly? StartTime { get; set; }
|
||||||
+18
-307
@@ -1,14 +1,8 @@
|
|||||||
@page "/settings"
|
@page "/settings"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize]
|
@inject TimetrackerService TrackerService
|
||||||
@inject ITimetrackerService TrackerService
|
@inject HolidayService HolidayService
|
||||||
@inject IHolidayService HolidayService
|
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject NavigationManager Nav
|
|
||||||
@inject HttpClient Http
|
|
||||||
|
|
||||||
|
|
||||||
<PageTitle>Einstellungen – Timetracker</PageTitle>
|
<PageTitle>Einstellungen – Timetracker</PageTitle>
|
||||||
|
|
||||||
@@ -23,12 +17,13 @@ else
|
|||||||
<MudStack Spacing="4">
|
<MudStack Spacing="4">
|
||||||
|
|
||||||
@* ── Header ── *@
|
@* ── Header ── *@
|
||||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl mud-theme-primary">
|
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||||
|
Style="background: linear-gradient(135deg, #3F51B5 0%, #1A237E 100%); color:white;">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Settings" Style="color: var(--mud-palette-primary-text); font-size:2rem" />
|
<MudIcon Icon="@Icons.Material.Filled.Settings" Style="color:white; font-size:2rem" />
|
||||||
<MudStack Spacing="0">
|
<MudStack Spacing="0">
|
||||||
<MudText Typo="Typo.h5" Style="color: var(--mud-palette-primary-text); font-weight:700">Einstellungen</MudText>
|
<MudText Typo="Typo.h5" Style="color:white; font-weight:700">Einstellungen</MudText>
|
||||||
<MudText Typo="Typo.caption" Style="color: var(--mud-palette-primary-text); opacity: 0.72">
|
<MudText Typo="Typo.caption" Style="color:rgba(255,255,255,0.72)">
|
||||||
Arbeitszeit, Arbeitstage und Urlaub konfigurieren
|
Arbeitszeit, Arbeitstage und Urlaub konfigurieren
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
@@ -131,138 +126,6 @@ else
|
|||||||
</MudCard>
|
</MudCard>
|
||||||
</MudItem>
|
</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>
|
|
||||||
|
|
||||||
@* ── Hilfe & Einführung ── *@
|
|
||||||
<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.HelpOutline" Color="Color.Primary" />
|
|
||||||
<MudText Typo="Typo.h6" Style="font-weight:600">Hilfe & Einführung</MudText>
|
|
||||||
</MudStack>
|
|
||||||
</CardHeaderContent>
|
|
||||||
</MudCardHeader>
|
|
||||||
<MudCardContent>
|
|
||||||
<MudStack Spacing="4">
|
|
||||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
|
||||||
Wenn du die interaktive Einführung (Onboarding Tour) noch einmal sehen möchtest, kannst du sie hier zurücksetzen und neu starten.
|
|
||||||
</MudText>
|
|
||||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.PlayCircleOutline"
|
|
||||||
OnClick="RepeatOnboarding"
|
|
||||||
Style="max-width:250px;">
|
|
||||||
Onboarding wiederholen
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
</MudCardContent>
|
|
||||||
</MudCard>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
@if (_isTenantAdmin)
|
|
||||||
{
|
|
||||||
@* ── Firmen-Einstellungen (Branding) ── *@
|
|
||||||
<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.Business" Color="Color.Primary" />
|
|
||||||
<MudText Typo="Typo.h6" Style="font-weight:600">Firmen-Einstellungen</MudText>
|
|
||||||
</MudStack>
|
|
||||||
</CardHeaderContent>
|
|
||||||
</MudCardHeader>
|
|
||||||
<MudCardContent>
|
|
||||||
<MudStack Spacing="4">
|
|
||||||
<MudTextField @bind-Value="_tenantName"
|
|
||||||
Label="Firmenname"
|
|
||||||
Variant="Variant.Outlined"
|
|
||||||
HelperText="Der offizielle Name deiner Firma" />
|
|
||||||
|
|
||||||
<MudStack Row="true" Spacing="4">
|
|
||||||
<MudColorPicker @bind-Text="_tenantPrimaryColor"
|
|
||||||
Label="Primärfarbe"
|
|
||||||
Variant="Variant.Outlined"
|
|
||||||
Style="@($"color: {_tenantPrimaryColor};")"
|
|
||||||
ColorPickerMode="ColorPickerMode.HEX" />
|
|
||||||
|
|
||||||
<MudColorPicker @bind-Text="_tenantSecondaryColor"
|
|
||||||
Label="Sekundärfarbe"
|
|
||||||
Variant="Variant.Outlined"
|
|
||||||
Style="@($"color: {_tenantSecondaryColor};")"
|
|
||||||
ColorPickerMode="ColorPickerMode.HEX" />
|
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
|
||||||
StartIcon="@Icons.Material.Filled.Save"
|
|
||||||
OnClick="SaveTenantSettings"
|
|
||||||
Style="max-width:250px;">
|
|
||||||
Firma speichern
|
|
||||||
</MudButton>
|
|
||||||
</MudStack>
|
|
||||||
</MudCardContent>
|
|
||||||
</MudCard>
|
|
||||||
</MudItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
|
|
||||||
@* ── Speichern-Button ── *@
|
@* ── Speichern-Button ── *@
|
||||||
@@ -511,60 +374,7 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public AppSettings? PersistentSettings { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public TenantInfo? PersistentTenantInfo { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<VacationDay>? PersistentVacationDays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<PublicHoliday>? PersistentHolHolidays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentTenantName { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentTenantPrimaryColor { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentTenantSecondaryColor { get; set; }
|
|
||||||
|
|
||||||
private AppSettings? _settings;
|
private AppSettings? _settings;
|
||||||
private int _userId;
|
|
||||||
private bool _isTenantAdmin;
|
|
||||||
private TenantInfo? _tenantInfo;
|
|
||||||
private string _tenantName = "";
|
|
||||||
private string _tenantPrimaryColor = "#0EA5E9";
|
|
||||||
private string _tenantSecondaryColor = "#0EA5E9";
|
|
||||||
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 int _vacYear = DateTime.Today.Year;
|
||||||
private List<VacationDay> _vacationDays = [];
|
private List<VacationDay> _vacationDays = [];
|
||||||
private DateTime? _newVacDateFrom;
|
private DateTime? _newVacDateFrom;
|
||||||
@@ -592,68 +402,14 @@ else
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (PersistentSettings != null)
|
_settings = await TrackerService.GetSettingsAsync();
|
||||||
{
|
await LoadVacations();
|
||||||
_settings = PersistentSettings;
|
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||||
_tenantInfo = PersistentTenantInfo;
|
|
||||||
_vacationDays = PersistentVacationDays ?? [];
|
|
||||||
_holHolidays = PersistentHolHolidays ?? [];
|
|
||||||
_tenantName = PersistentTenantName ?? "";
|
|
||||||
_tenantPrimaryColor = PersistentTenantPrimaryColor ?? "#0EA5E9";
|
|
||||||
_tenantSecondaryColor = PersistentTenantSecondaryColor ?? "#0EA5E9";
|
|
||||||
}
|
|
||||||
|
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
|
||||||
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
|
|
||||||
if (claim == null) return;
|
|
||||||
_userId = int.Parse(claim.Value);
|
|
||||||
|
|
||||||
if (PersistentSettings != null)
|
|
||||||
{
|
|
||||||
_isTenantAdmin = authState.User.IsInRole("TenantAdmin") || authState.User.HasClaim("IsTenantAdmin", "true");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_isTenantAdmin = authState.User.IsInRole("TenantAdmin") || authState.User.HasClaim("IsTenantAdmin", "true");
|
|
||||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
|
||||||
|
|
||||||
if (_isTenantAdmin)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_tenantInfo = await Http.GetFromJsonAsync<TenantInfo>("api/tenant/current");
|
|
||||||
if (_tenantInfo != null)
|
|
||||||
{
|
|
||||||
_tenantName = _tenantInfo.Name ?? "";
|
|
||||||
_tenantPrimaryColor = _tenantInfo.PrimaryColor ?? "#0EA5E9";
|
|
||||||
_tenantSecondaryColor = _tenantInfo.SecondaryColor ?? "#0EA5E9";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Fehler beim Laden der Firmen-Einstellungen: {ex.Message}", Severity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var loadVacationsTask = LoadVacations();
|
|
||||||
var loadHolidaysTask = HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
|
|
||||||
|
|
||||||
await Task.WhenAll(loadVacationsTask, loadHolidaysTask);
|
|
||||||
_holHolidays = await loadHolidaysTask;
|
|
||||||
|
|
||||||
PersistentSettings = _settings;
|
|
||||||
PersistentTenantInfo = _tenantInfo;
|
|
||||||
PersistentVacationDays = _vacationDays;
|
|
||||||
PersistentHolHolidays = _holHolidays;
|
|
||||||
PersistentTenantName = _tenantName;
|
|
||||||
PersistentTenantPrimaryColor = _tenantPrimaryColor;
|
|
||||||
PersistentTenantSecondaryColor = _tenantSecondaryColor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadVacations()
|
private async Task LoadVacations()
|
||||||
{
|
{
|
||||||
_vacationDays = await TrackerService.GetVacationDaysAsync(_userId, _vacYear);
|
_vacationDays = await TrackerService.GetVacationDaysAsync(_vacYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ChangeYear(int delta)
|
private async Task ChangeYear(int delta)
|
||||||
@@ -666,8 +422,6 @@ else
|
|||||||
{
|
{
|
||||||
if (_settings == null) return;
|
if (_settings == null) return;
|
||||||
await TrackerService.SaveSettingsAsync(_settings);
|
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);
|
Snackbar.Add("Einstellungen gespeichert", Severity.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -684,7 +438,7 @@ else
|
|||||||
{
|
{
|
||||||
if (_settings!.IsWorkDay(current.DayOfWeek))
|
if (_settings!.IsWorkDay(current.DayOfWeek))
|
||||||
{
|
{
|
||||||
await TrackerService.AddVacationDayAsync(new VacationDay { UserId = _userId, Date = current, Note = note });
|
await TrackerService.AddVacationDayAsync(new VacationDay { Date = current, Note = note });
|
||||||
added++;
|
added++;
|
||||||
}
|
}
|
||||||
current = current.AddDays(1);
|
current = current.AddDays(1);
|
||||||
@@ -698,7 +452,7 @@ else
|
|||||||
|
|
||||||
private async Task RemoveVacation(int id)
|
private async Task RemoveVacation(int id)
|
||||||
{
|
{
|
||||||
await TrackerService.RemoveVacationDayAsync(_userId, id);
|
await TrackerService.RemoveVacationDayAsync(id);
|
||||||
await LoadVacations();
|
await LoadVacations();
|
||||||
Snackbar.Add("Urlaubstag entfernt", Severity.Info);
|
Snackbar.Add("Urlaubstag entfernt", Severity.Info);
|
||||||
}
|
}
|
||||||
@@ -707,14 +461,14 @@ else
|
|||||||
private async Task ChangeHolYear(int delta)
|
private async Task ChangeHolYear(int delta)
|
||||||
{
|
{
|
||||||
_holYear += delta;
|
_holYear += delta;
|
||||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
|
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FetchHolidays()
|
private async Task FetchHolidays()
|
||||||
{
|
{
|
||||||
_fetchingHolidays = true;
|
_fetchingHolidays = true;
|
||||||
var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear);
|
var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear);
|
||||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
|
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||||
_fetchingHolidays = false;
|
_fetchingHolidays = false;
|
||||||
Snackbar.Add(message, success ? Severity.Success : Severity.Error);
|
Snackbar.Add(message, success ? Severity.Success : Severity.Error);
|
||||||
}
|
}
|
||||||
@@ -722,53 +476,10 @@ else
|
|||||||
private async Task DeleteHoliday(int id)
|
private async Task DeleteHoliday(int id)
|
||||||
{
|
{
|
||||||
await HolidayService.DeleteAsync(id);
|
await HolidayService.DeleteAsync(id);
|
||||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
|
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||||
Snackbar.Add("Feiertag entfernt", Severity.Info);
|
Snackbar.Add("Feiertag entfernt", Severity.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RepeatOnboarding()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "showOnboarding", "true");
|
|
||||||
Snackbar.Add("Onboarding zurückgesetzt. Leite weiter zur Startseite...", Severity.Info);
|
|
||||||
Nav.NavigateTo("/");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Snackbar.Add($"Fehler beim Zurücksetzen des Onboardings: {ex.Message}", Severity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SaveTenantSettings()
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(_tenantName))
|
|
||||||
{
|
|
||||||
Snackbar.Add("Bitte einen Firmennamen eingeben.", Severity.Warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dto = new TenantSettingsDto
|
|
||||||
{
|
|
||||||
Name = _tenantName.Trim(),
|
|
||||||
PrimaryColor = _tenantPrimaryColor,
|
|
||||||
SecondaryColor = _tenantSecondaryColor
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await Http.PostAsJsonAsync("api/tracker/tenant/settings", dto);
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
Snackbar.Add("Firmen-Einstellungen gespeichert. Lade Seite neu...", Severity.Success);
|
|
||||||
await Task.Delay(1000);
|
|
||||||
Nav.NavigateTo(Nav.Uri, forceLoad: true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var err = await response.Content.ReadAsStringAsync();
|
|
||||||
Snackbar.Add($"Fehler beim Speichern: {err}", Severity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string FormatHours(double hours)
|
private static string FormatHours(double hours)
|
||||||
{
|
{
|
||||||
var ts = TimeSpan.FromHours(hours);
|
var ts = TimeSpan.FromHours(hours);
|
||||||
+9
-66
@@ -1,10 +1,8 @@
|
|||||||
@page "/urlaub-maximizer"
|
@page "/urlaub-maximizer"
|
||||||
@rendermode InteractiveWebAssembly
|
@rendermode InteractiveServer
|
||||||
@attribute [Authorize]
|
@inject TimetrackerService TrackerService
|
||||||
@inject ITimetrackerService TrackerService
|
@inject HolidayService HolidayService
|
||||||
@inject IHolidayService HolidayService
|
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject AuthenticationStateProvider AuthStateProvider
|
|
||||||
|
|
||||||
<PageTitle>Urlaubs-Maximizer – Timetracker</PageTitle>
|
<PageTitle>Urlaubs-Maximizer – Timetracker</PageTitle>
|
||||||
|
|
||||||
@@ -21,7 +19,7 @@ else
|
|||||||
|
|
||||||
@* ── Header ── *@
|
@* ── Header ── *@
|
||||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||||
Style="background: #C2410C; color:white;">
|
Style="background: linear-gradient(135deg, #F57F17 0%, #E65100 100%); color:white;">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||||
Style="color:white" Size="Size.Large" OnClick="PrevYear" />
|
Style="color:white" Size="Size.Large" OnClick="PrevYear" />
|
||||||
@@ -226,27 +224,6 @@ else
|
|||||||
@code {
|
@code {
|
||||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public AppSettings? PersistentSettings { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public Dictionary<DateOnly, string>? PersistentHolidays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<DateOnly>? PersistentVacationList { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentRemainingDays { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public List<Suggestion>? PersistentSuggestions { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public string? PersistentSubLabel { get; set; }
|
|
||||||
|
|
||||||
[PersistentState(AllowUpdates = true)]
|
|
||||||
public int? PersistentYear { get; set; }
|
|
||||||
|
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private int _year = DateTime.Today.Year;
|
private int _year = DateTime.Today.Year;
|
||||||
private AppSettings _settings = new();
|
private AppSettings _settings = new();
|
||||||
@@ -255,52 +232,18 @@ else
|
|||||||
private int _remainingDays;
|
private int _remainingDays;
|
||||||
private List<Suggestion> _suggestions = [];
|
private List<Suggestion> _suggestions = [];
|
||||||
private string _subLabel = "";
|
private string _subLabel = "";
|
||||||
private int _userId;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (PersistentSettings != null && PersistentSuggestions != null)
|
_settings = await TrackerService.GetSettingsAsync();
|
||||||
{
|
|
||||||
_settings = PersistentSettings;
|
|
||||||
_holidays = PersistentHolidays ?? [];
|
|
||||||
_vacationSet = (PersistentVacationList ?? []).ToHashSet();
|
|
||||||
_remainingDays = PersistentRemainingDays ?? 0;
|
|
||||||
_suggestions = PersistentSuggestions;
|
|
||||||
_subLabel = PersistentSubLabel ?? "";
|
|
||||||
_year = PersistentYear ?? DateTime.Today.Year;
|
|
||||||
_loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
|
||||||
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
|
|
||||||
if (claim == null) return;
|
|
||||||
_userId = int.Parse(claim.Value);
|
|
||||||
|
|
||||||
if (PersistentSettings == null || PersistentSuggestions == null)
|
|
||||||
{
|
|
||||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
|
||||||
await LoadYear();
|
await LoadYear();
|
||||||
|
|
||||||
PersistentSettings = _settings;
|
|
||||||
PersistentHolidays = _holidays;
|
|
||||||
PersistentVacationList = _vacationSet.ToList();
|
|
||||||
PersistentRemainingDays = _remainingDays;
|
|
||||||
PersistentSuggestions = _suggestions;
|
|
||||||
PersistentSubLabel = _subLabel;
|
|
||||||
PersistentYear = _year;
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadYear()
|
private async Task LoadYear()
|
||||||
{
|
{
|
||||||
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
|
var holidays = await HolidayService.GetHolidaysAsync(_year);
|
||||||
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year);
|
var vacations = await TrackerService.GetVacationDaysAsync(_year);
|
||||||
|
|
||||||
await Task.WhenAll(holidaysTask, vacationsTask);
|
|
||||||
|
|
||||||
var holidays = await holidaysTask;
|
|
||||||
var vacations = await vacationsTask;
|
|
||||||
_holidays = holidays.ToDictionary(h => h.Date, h => h.Name);
|
_holidays = holidays.ToDictionary(h => h.Date, h => h.Name);
|
||||||
_vacationSet = vacations.Select(v => v.Date).ToHashSet();
|
_vacationSet = vacations.Select(v => v.Date).ToHashSet();
|
||||||
_remainingDays = Math.Max(0, _settings.VacationDaysPerYear - vacations.Count);
|
_remainingDays = Math.Max(0, _settings.VacationDaysPerYear - vacations.Count);
|
||||||
@@ -319,7 +262,7 @@ else
|
|||||||
private async Task TakeSuggestion(Suggestion s)
|
private async Task TakeSuggestion(Suggestion s)
|
||||||
{
|
{
|
||||||
foreach (var d in s.VacationDaysToTake.Where(d => !_vacationSet.Contains(d)))
|
foreach (var d in s.VacationDaysToTake.Where(d => !_vacationSet.Contains(d)))
|
||||||
await TrackerService.AddVacationDayAsync(new VacationDay { UserId = _userId, Date = d, Note = "Urlaubs-Maximizer" });
|
await TrackerService.AddVacationDayAsync(new VacationDay { Date = d, Note = "Urlaubs-Maximizer" });
|
||||||
await LoadYear();
|
await LoadYear();
|
||||||
var word = s.VacationDaysNeeded == 1 ? "Urlaubstag" : "Urlaubstage";
|
var word = s.VacationDaysNeeded == 1 ? "Urlaubstag" : "Urlaubstage";
|
||||||
Snackbar.Add($"{s.VacationDaysNeeded} {word} eingetragen – {s.TotalFreeDays} Tage frei!", Severity.Success);
|
Snackbar.Add($"{s.VacationDaysNeeded} {word} eingetragen – {s.TotalFreeDays} Tage frei!", Severity.Success);
|
||||||
@@ -328,7 +271,7 @@ else
|
|||||||
// ── Algorithmus ──────────────────────────────────────────────────────
|
// ── Algorithmus ──────────────────────────────────────────────────────
|
||||||
private enum DayKind { Free, WorkAvailable, WorkTaken }
|
private enum DayKind { Free, WorkAvailable, WorkTaken }
|
||||||
|
|
||||||
public sealed record Suggestion(
|
private sealed record Suggestion(
|
||||||
DateOnly SpanStart,
|
DateOnly SpanStart,
|
||||||
DateOnly SpanEnd,
|
DateOnly SpanEnd,
|
||||||
List<DateOnly> VacationDaysToTake,
|
List<DateOnly> VacationDaysToTake,
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<Router AppAssembly="typeof(Program).Assembly" NotFoundPage="typeof(Pages.NotFound)">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
@using System.Net.Http
|
@using System.Net.Http
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
@using System.Security.Claims
|
|
||||||
@using Microsoft.AspNetCore.Authorization
|
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
|
||||||
@using Microsoft.AspNetCore.Components.Forms
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
@@ -10,8 +7,7 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using timetracker
|
@using timetracker
|
||||||
@using timetracker.Client.Components
|
@using timetracker.Components
|
||||||
@using timetracker.Client.Components.Layout
|
@using timetracker.Components.Layout
|
||||||
@using timetracker.Data
|
@using timetracker.Data
|
||||||
@using timetracker.Shared
|
|
||||||
@using MudBlazor
|
@using MudBlazor
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
namespace timetracker.Shared;
|
namespace timetracker.Data;
|
||||||
|
|
||||||
public class AppSettings
|
public class AppSettings
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int UserId { get; set; }
|
|
||||||
public int? TenantId { get; set; }
|
|
||||||
public double DailyTargetHours { get; set; } = 7.5;
|
public double DailyTargetHours { get; set; } = 7.5;
|
||||||
public int MinimumBreakMinutes { get; set; } = 30;
|
public int MinimumBreakMinutes { get; set; } = 30;
|
||||||
public int VacationDaysPerYear { 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
|
// Arbeitstage
|
||||||
public bool WorkMonday { get; set; } = true;
|
public bool WorkMonday { get; set; } = true;
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
namespace timetracker.Shared;
|
namespace timetracker.Data;
|
||||||
|
|
||||||
public class BreakEntry
|
public class BreakEntry
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int WorkDayId { get; set; }
|
public int WorkDayId { get; set; }
|
||||||
[System.Text.Json.Serialization.JsonIgnore]
|
|
||||||
public WorkDay WorkDay { get; set; } = null!;
|
public WorkDay WorkDay { get; set; } = null!;
|
||||||
public TimeOnly? StartTime { get; set; }
|
public TimeOnly? StartTime { get; set; }
|
||||||
public TimeOnly? EndTime { get; set; }
|
public TimeOnly? EndTime { get; set; }
|
||||||
@@ -1,30 +1,20 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using timetracker.Shared;
|
|
||||||
|
|
||||||
namespace timetracker.Data;
|
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";
|
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();
|
await using var db = await factory.CreateDbContextAsync();
|
||||||
var holidays = await db.PublicHolidays
|
return await db.PublicHolidays
|
||||||
.Where(h => h.Date.Year == year)
|
.Where(h => h.Date.Year == year)
|
||||||
.OrderBy(h => h.Date)
|
.OrderBy(h => h.Date)
|
||||||
.ToListAsync();
|
.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)
|
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
|
.Select(h => new PublicHoliday
|
||||||
{
|
{
|
||||||
Date = DateOnly.Parse(h.Date),
|
Date = DateOnly.Parse(h.Date),
|
||||||
Name = h.LocalName,
|
Name = h.LocalName
|
||||||
Counties = h.Counties != null && h.Counties.Count > 0 ? string.Join(",", h.Counties) : null
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
@@ -75,9 +64,5 @@ public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, Htt
|
|||||||
|
|
||||||
[JsonPropertyName("localName")]
|
[JsonPropertyName("localName")]
|
||||||
public string LocalName { get; set; } = "";
|
public string LocalName { get; set; } = "";
|
||||||
|
|
||||||
[JsonPropertyName("counties")]
|
|
||||||
public List<string>? Counties { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+20
-55
@@ -2,7 +2,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using timetracker.Data;
|
using timetracker.Data;
|
||||||
|
|
||||||
@@ -11,11 +10,9 @@ using timetracker.Data;
|
|||||||
namespace timetracker.Data.Migrations
|
namespace timetracker.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(TimetrackerDbContext))]
|
[DbContext(typeof(TimetrackerDbContext))]
|
||||||
[Migration("20260522081459_AddMultiUser")]
|
partial class TimetrackerDbContextModelSnapshot : ModelSnapshot
|
||||||
partial class AddMultiUser
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.8");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.8");
|
||||||
@@ -32,9 +29,6 @@ namespace timetracker.Data.Migrations
|
|||||||
b.Property<int>("MinimumBreakMinutes")
|
b.Property<int>("MinimumBreakMinutes")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<int>("VacationDaysPerYear")
|
b.Property<int>("VacationDaysPerYear")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
@@ -64,6 +58,24 @@ namespace timetracker.Data.Migrations
|
|||||||
b.ToTable("AppSettings");
|
b.ToTable("AppSettings");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("PublicHolidays");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("timetracker.Data.BreakEntry", b =>
|
modelBuilder.Entity("timetracker.Data.BreakEntry", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -86,47 +98,6 @@ namespace timetracker.Data.Migrations
|
|||||||
b.ToTable("BreakEntries");
|
b.ToTable("BreakEntries");
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<DateOnly>("Date")
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("PublicHolidays");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("timetracker.Data.User", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordHash")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("PasswordSalt")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<string>("Username")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("timetracker.Data.VacationDay", b =>
|
modelBuilder.Entity("timetracker.Data.VacationDay", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -139,9 +110,6 @@ namespace timetracker.Data.Migrations
|
|||||||
b.Property<string>("Note")
|
b.Property<string>("Note")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("VacationDays");
|
b.ToTable("VacationDays");
|
||||||
@@ -162,9 +130,6 @@ namespace timetracker.Data.Migrations
|
|||||||
b.Property<TimeOnly?>("StartTime")
|
b.Property<TimeOnly?>("StartTime")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("INTEGER");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("WorkDays");
|
b.ToTable("WorkDays");
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
namespace timetracker.Shared;
|
namespace timetracker.Data;
|
||||||
|
|
||||||
public class PublicHoliday
|
public class PublicHoliday
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
public string Name { get; set; } = "";
|
public string Name { get; set; } = "";
|
||||||
public string? Counties { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace timetracker.Data;
|
||||||
|
|
||||||
|
public class TimetrackerDbContext(DbContextOptions<TimetrackerDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
public DbSet<WorkDay> WorkDays => Set<WorkDay>();
|
||||||
|
public DbSet<BreakEntry> BreakEntries => Set<BreakEntry>();
|
||||||
|
public DbSet<AppSettings> AppSettings => Set<AppSettings>();
|
||||||
|
public DbSet<VacationDay> VacationDays => Set<VacationDay>();
|
||||||
|
public DbSet<PublicHoliday> PublicHolidays => Set<PublicHoliday>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace timetracker.Data;
|
||||||
|
|
||||||
|
public class TimetrackerService(IDbContextFactory<TimetrackerDbContext> factory)
|
||||||
|
{
|
||||||
|
public async Task<List<WorkDay>> GetWeekAsync(DateOnly monday)
|
||||||
|
{
|
||||||
|
await using var db = await factory.CreateDbContextAsync();
|
||||||
|
return await db.WorkDays
|
||||||
|
.Include(w => w.Breaks)
|
||||||
|
.Where(w => 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.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()
|
||||||
|
{
|
||||||
|
await using var db = await factory.CreateDbContextAsync();
|
||||||
|
return await db.AppSettings.FindAsync(1) ?? new AppSettings { Id = 1 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveSettingsAsync(AppSettings settings)
|
||||||
|
{
|
||||||
|
await using var db = await factory.CreateDbContextAsync();
|
||||||
|
settings.Id = 1;
|
||||||
|
var existing = await db.AppSettings.FindAsync(1);
|
||||||
|
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 year)
|
||||||
|
{
|
||||||
|
await using var db = await factory.CreateDbContextAsync();
|
||||||
|
return await db.VacationDays
|
||||||
|
.Where(v => 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.Date == vacationDay.Date);
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
vacationDay.Id = 0;
|
||||||
|
db.VacationDays.Add(vacationDay);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveVacationDayAsync(int id)
|
||||||
|
{
|
||||||
|
await using var db = await factory.CreateDbContextAsync();
|
||||||
|
var v = await db.VacationDays.FindAsync(id);
|
||||||
|
if (v != null)
|
||||||
|
{
|
||||||
|
db.VacationDays.Remove(v);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Gleitzeitkonto ───────────────────────────────────────────────────
|
||||||
|
public async Task<TimeSpan> GetTotalOvertimeAsync(AppSettings settings)
|
||||||
|
{
|
||||||
|
await using var db = await factory.CreateDbContextAsync();
|
||||||
|
var allDays = await db.WorkDays
|
||||||
|
.Include(w => w.Breaks)
|
||||||
|
.Where(w => w.StartTime != null && w.EndTime != null)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
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()));
|
||||||
|
total += gross - breakTotal - TimeSpan.FromHours(settings.DailyTargetHours);
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Monatsübersicht ───────────────────────────────────────────────────
|
||||||
|
public async Task<List<WorkDay>> GetMonthAsync(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.Date >= from && w.Date < to)
|
||||||
|
.OrderBy(w => w.Date)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
namespace timetracker.Shared;
|
namespace timetracker.Data;
|
||||||
|
|
||||||
public class VacationDay
|
public class VacationDay
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int UserId { get; set; }
|
|
||||||
public int? TenantId { get; set; }
|
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
public string? Note { get; set; }
|
public string? Note { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
namespace timetracker.Shared;
|
namespace timetracker.Data;
|
||||||
|
|
||||||
public class WorkDay
|
public class WorkDay
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public int UserId { get; set; }
|
|
||||||
public int? TenantId { get; set; }
|
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
public TimeOnly? StartTime { get; set; }
|
public TimeOnly? StartTime { get; set; }
|
||||||
public TimeOnly? EndTime { get; set; }
|
public TimeOnly? EndTime { get; set; }
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# ── Build Stage ──────────────────────────────────────────────────────────────
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
# Keine Unterordner mehr beim Kopieren!
|
||||||
|
COPY timetracker.csproj ./
|
||||||
|
RUN dotnet restore timetracker.csproj
|
||||||
|
|
||||||
|
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 .
|
||||||
|
|
||||||
|
# Verzeichnis für die SQLite-Datenbank
|
||||||
|
RUN mkdir -p /data
|
||||||
|
|
||||||
|
ENV ASPNETCORE_HTTP_PORTS=8080
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
ENV TIMETRACKER_DB_PATH=/data/timetracker.db
|
||||||
|
ENV EnableHttpsRedirect=false
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "timetracker.dll"]
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MudBlazor.Services;
|
||||||
|
using timetracker.Components;
|
||||||
|
using timetracker.Data;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// 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.UseAntiforgery();
|
||||||
|
|
||||||
|
app.MapStaticAssets();
|
||||||
|
app.MapRazorComponents<App>()
|
||||||
|
.AddInteractiveServerRenderMode();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
@@ -1,795 +0,0 @@
|
|||||||
bash-5.2# ls
|
|
||||||
timetracker-20260626.log
|
|
||||||
bash-5.2# cat timetracker-20260626.log
|
|
||||||
2026-06-26 13:31:25.632 +00:00 [INF] File logging active. Writing logs to /app/logs
|
|
||||||
2026-06-26 13:31:26.019 +00:00 [WRN] Entity 'WorkDay' has a global query filter defined and is the required end of a relationship with the entity 'BreakEntry'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.
|
|
||||||
2026-06-26 13:31:26.249 +00:00 [INF] Executed DbCommand (3ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:26.270 +00:00 [INF] Executed DbCommand (2ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT 1
|
|
||||||
2026-06-26 13:31:26.341 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
||||||
"MigrationId" character varying(150) NOT NULL,
|
|
||||||
"ProductVersion" character varying(32) NOT NULL,
|
|
||||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
|
||||||
);
|
|
||||||
2026-06-26 13:31:26.351 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
LOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODE
|
|
||||||
2026-06-26 13:31:26.352 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:26.355 +00:00 [INF] No migrations were applied. The database is already up to date.
|
|
||||||
2026-06-26 13:31:26.548 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT u."Id", u."IsTenantAdmin", u."PasswordHash", u."PasswordSalt", u."TenantId", u."Username"
|
|
||||||
FROM "Users" AS u
|
|
||||||
WHERE u."TenantId" IS NULL AND u."Username" = 'marc'
|
|
||||||
LIMIT 1
|
|
||||||
2026-06-26 13:31:26.705 +00:00 [ERR] Failed executing DbCommand (6ms) [Parameters=[@p0='?' (DbType = Boolean), @p1='?', @p2='?', @p3='?' (DbType = Int32), @p4='?'], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
VALUES (@p0, @p1, @p2, @p3, @p4)
|
|
||||||
RETURNING "Id";
|
|
||||||
2026-06-26 13:31:26.719 +00:00 [ERR] An exception occurred in the database while saving changes for context type 'timetracker.Data.TimetrackerDbContext'.
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
2026-06-26 13:31:28.470 +00:00 [INF] File logging active. Writing logs to /app/logs
|
|
||||||
2026-06-26 13:31:28.848 +00:00 [WRN] Entity 'WorkDay' has a global query filter defined and is the required end of a relationship with the entity 'BreakEntry'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.
|
|
||||||
2026-06-26 13:31:29.078 +00:00 [INF] Executed DbCommand (3ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:29.100 +00:00 [INF] Executed DbCommand (2ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT 1
|
|
||||||
2026-06-26 13:31:29.170 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
||||||
"MigrationId" character varying(150) NOT NULL,
|
|
||||||
"ProductVersion" character varying(32) NOT NULL,
|
|
||||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
|
||||||
);
|
|
||||||
2026-06-26 13:31:29.180 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
LOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODE
|
|
||||||
2026-06-26 13:31:29.182 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:29.185 +00:00 [INF] No migrations were applied. The database is already up to date.
|
|
||||||
2026-06-26 13:31:29.376 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT u."Id", u."IsTenantAdmin", u."PasswordHash", u."PasswordSalt", u."TenantId", u."Username"
|
|
||||||
FROM "Users" AS u
|
|
||||||
WHERE u."TenantId" IS NULL AND u."Username" = 'marc'
|
|
||||||
LIMIT 1
|
|
||||||
2026-06-26 13:31:29.537 +00:00 [ERR] Failed executing DbCommand (6ms) [Parameters=[@p0='?' (DbType = Boolean), @p1='?', @p2='?', @p3='?' (DbType = Int32), @p4='?'], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
VALUES (@p0, @p1, @p2, @p3, @p4)
|
|
||||||
RETURNING "Id";
|
|
||||||
2026-06-26 13:31:29.548 +00:00 [ERR] An exception occurred in the database while saving changes for context type 'timetracker.Data.TimetrackerDbContext'.
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
2026-06-26 13:31:31.379 +00:00 [INF] File logging active. Writing logs to /app/logs
|
|
||||||
2026-06-26 13:31:31.760 +00:00 [WRN] Entity 'WorkDay' has a global query filter defined and is the required end of a relationship with the entity 'BreakEntry'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.
|
|
||||||
2026-06-26 13:31:31.990 +00:00 [INF] Executed DbCommand (3ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:32.011 +00:00 [INF] Executed DbCommand (2ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT 1
|
|
||||||
2026-06-26 13:31:32.082 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
||||||
"MigrationId" character varying(150) NOT NULL,
|
|
||||||
"ProductVersion" character varying(32) NOT NULL,
|
|
||||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
|
||||||
);
|
|
||||||
2026-06-26 13:31:32.091 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
LOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODE
|
|
||||||
2026-06-26 13:31:32.094 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:32.097 +00:00 [INF] No migrations were applied. The database is already up to date.
|
|
||||||
2026-06-26 13:31:32.290 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT u."Id", u."IsTenantAdmin", u."PasswordHash", u."PasswordSalt", u."TenantId", u."Username"
|
|
||||||
FROM "Users" AS u
|
|
||||||
WHERE u."TenantId" IS NULL AND u."Username" = 'marc'
|
|
||||||
LIMIT 1
|
|
||||||
2026-06-26 13:31:32.452 +00:00 [ERR] Failed executing DbCommand (6ms) [Parameters=[@p0='?' (DbType = Boolean), @p1='?', @p2='?', @p3='?' (DbType = Int32), @p4='?'], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
VALUES (@p0, @p1, @p2, @p3, @p4)
|
|
||||||
RETURNING "Id";
|
|
||||||
2026-06-26 13:31:32.462 +00:00 [ERR] An exception occurred in the database while saving changes for context type 'timetracker.Data.TimetrackerDbContext'.
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
2026-06-26 13:31:34.341 +00:00 [INF] File logging active. Writing logs to /app/logs
|
|
||||||
2026-06-26 13:31:34.731 +00:00 [WRN] Entity 'WorkDay' has a global query filter defined and is the required end of a relationship with the entity 'BreakEntry'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.
|
|
||||||
2026-06-26 13:31:34.964 +00:00 [INF] Executed DbCommand (3ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:34.986 +00:00 [INF] Executed DbCommand (2ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT 1
|
|
||||||
2026-06-26 13:31:35.057 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
||||||
"MigrationId" character varying(150) NOT NULL,
|
|
||||||
"ProductVersion" character varying(32) NOT NULL,
|
|
||||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
|
||||||
);
|
|
||||||
2026-06-26 13:31:35.067 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
LOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODE
|
|
||||||
2026-06-26 13:31:35.068 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:35.072 +00:00 [INF] No migrations were applied. The database is already up to date.
|
|
||||||
2026-06-26 13:31:35.267 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT u."Id", u."IsTenantAdmin", u."PasswordHash", u."PasswordSalt", u."TenantId", u."Username"
|
|
||||||
FROM "Users" AS u
|
|
||||||
WHERE u."TenantId" IS NULL AND u."Username" = 'marc'
|
|
||||||
LIMIT 1
|
|
||||||
2026-06-26 13:31:35.426 +00:00 [ERR] Failed executing DbCommand (7ms) [Parameters=[@p0='?' (DbType = Boolean), @p1='?', @p2='?', @p3='?' (DbType = Int32), @p4='?'], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
VALUES (@p0, @p1, @p2, @p3, @p4)
|
|
||||||
RETURNING "Id";
|
|
||||||
2026-06-26 13:31:35.440 +00:00 [ERR] An exception occurred in the database while saving changes for context type 'timetracker.Data.TimetrackerDbContext'.
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
2026-06-26 13:31:37.974 +00:00 [INF] File logging active. Writing logs to /app/logs
|
|
||||||
2026-06-26 13:31:38.451 +00:00 [WRN] Entity 'WorkDay' has a global query filter defined and is the required end of a relationship with the entity 'BreakEntry'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.
|
|
||||||
2026-06-26 13:31:38.698 +00:00 [INF] Executed DbCommand (3ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:38.721 +00:00 [INF] Executed DbCommand (2ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT 1
|
|
||||||
2026-06-26 13:31:38.792 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
||||||
"MigrationId" character varying(150) NOT NULL,
|
|
||||||
"ProductVersion" character varying(32) NOT NULL,
|
|
||||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
|
||||||
);
|
|
||||||
2026-06-26 13:31:38.802 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
LOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODE
|
|
||||||
2026-06-26 13:31:38.803 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:38.807 +00:00 [INF] No migrations were applied. The database is already up to date.
|
|
||||||
2026-06-26 13:31:39.004 +00:00 [INF] Executed DbCommand (1ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT u."Id", u."IsTenantAdmin", u."PasswordHash", u."PasswordSalt", u."TenantId", u."Username"
|
|
||||||
FROM "Users" AS u
|
|
||||||
WHERE u."TenantId" IS NULL AND u."Username" = 'marc'
|
|
||||||
LIMIT 1
|
|
||||||
2026-06-26 13:31:39.163 +00:00 [ERR] Failed executing DbCommand (6ms) [Parameters=[@p0='?' (DbType = Boolean), @p1='?', @p2='?', @p3='?' (DbType = Int32), @p4='?'], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
VALUES (@p0, @p1, @p2, @p3, @p4)
|
|
||||||
RETURNING "Id";
|
|
||||||
2026-06-26 13:31:39.176 +00:00 [ERR] An exception occurred in the database while saving changes for context type 'timetracker.Data.TimetrackerDbContext'.
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
2026-06-26 13:31:41.413 +00:00 [INF] File logging active. Writing logs to /app/logs
|
|
||||||
2026-06-26 13:31:41.805 +00:00 [WRN] Entity 'WorkDay' has a global query filter defined and is the required end of a relationship with the entity 'BreakEntry'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.
|
|
||||||
2026-06-26 13:31:42.040 +00:00 [INF] Executed DbCommand (4ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:42.062 +00:00 [INF] Executed DbCommand (2ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT 1
|
|
||||||
2026-06-26 13:31:42.133 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
||||||
"MigrationId" character varying(150) NOT NULL,
|
|
||||||
"ProductVersion" character varying(32) NOT NULL,
|
|
||||||
CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId")
|
|
||||||
);
|
|
||||||
2026-06-26 13:31:42.143 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
LOCK TABLE "__EFMigrationsHistory" IN ACCESS EXCLUSIVE MODE
|
|
||||||
2026-06-26 13:31:42.144 +00:00 [INF] Executed DbCommand (0ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT "MigrationId", "ProductVersion"
|
|
||||||
FROM "__EFMigrationsHistory"
|
|
||||||
ORDER BY "MigrationId";
|
|
||||||
2026-06-26 13:31:42.148 +00:00 [INF] No migrations were applied. The database is already up to date.
|
|
||||||
2026-06-26 13:31:42.342 +00:00 [INF] Executed DbCommand (2ms) [Parameters=[], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
SELECT u."Id", u."IsTenantAdmin", u."PasswordHash", u."PasswordSalt", u."TenantId", u."Username"
|
|
||||||
FROM "Users" AS u
|
|
||||||
WHERE u."TenantId" IS NULL AND u."Username" = 'marc'
|
|
||||||
LIMIT 1
|
|
||||||
2026-06-26 13:31:42.501 +00:00 [ERR] Failed executing DbCommand (6ms) [Parameters=[@p0='?' (DbType = Boolean), @p1='?', @p2='?', @p3='?' (DbType = Int32), @p4='?'], CommandType='"Text"', CommandTimeout='30']
|
|
||||||
INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
VALUES (@p0, @p1, @p2, @p3, @p4)
|
|
||||||
RETURNING "Id";
|
|
||||||
2026-06-26 13:31:42.514 +00:00 [ERR] An exception occurred in the database while saving changes for context type 'timetracker.Data.TimetrackerDbContext'.
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes. See the inner exception for details.
|
|
||||||
---> Npgsql.PostgresException (0x80004005): 42804: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
|
|
||||||
POSITION: 103
|
|
||||||
at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
|
|
||||||
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder`1.StateMachineBox`1.System.Threading.Tasks.Sources.IValueTaskSource<TResult>.GetResult(Int16 token)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
Exception data:
|
|
||||||
Severity: ERROR
|
|
||||||
SqlState: 42804
|
|
||||||
MessageText: column "IsTenantAdmin" is of type integer but expression is of type boolean
|
|
||||||
Hint: You will need to rewrite or cast the expression.
|
|
||||||
Position: 103
|
|
||||||
File: parse_target.c
|
|
||||||
Line: 586
|
|
||||||
Routine: transformAssignedExpr
|
|
||||||
--- End of inner exception stack trace ---
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.ExecuteAsync(IEnumerable`1 commandBatches, IRelationalConnection connection, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabase.SaveChangesAsync(IList`1 entries, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(IList`1 entriesToSave, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChangesAsync(StateManager stateManager, Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
at Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.NpgsqlExecutionStrategy.ExecuteAsync[TState,TResult](TState state, Func`4 operation, Func`4 verifySucceeded, CancellationToken cancellationToken)
|
|
||||||
at Microsoft.EntityFrameworkCore.DbContext.SaveChangesAsync(Boolean acceptAllChangesOnSuccess, CancellationToken cancellationToken)
|
|
||||||
2026-06-26 13:31:46.331 +00:00 [INF] File logging active. Writing logs to /app/logs
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Und logs vom DB container:
|
|
||||||
|
|
||||||
2026-06-26 13:31:42.498 UTC [32] HINT: You will need to rewrite or cast the expression.
|
|
||||||
|
|
||||||
2026-06-26 13:31:42.498 UTC [32] STATEMENT: INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
|
|
||||||
RETURNING "Id"
|
|
||||||
|
|
||||||
2026-06-26 13:31:47.069 UTC [33] ERROR: syntax error at or near "public" at character 68
|
|
||||||
|
|
||||||
2026-06-26 13:31:47.069 UTC [33] QUERY: ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''public."Users_Id_seq"'')
|
|
||||||
|
|
||||||
2026-06-26 13:31:47.069 UTC [33] CONTEXT: PL/pgSQL function inline_code_block line 12 at EXECUTE
|
|
||||||
|
|
||||||
2026-06-26 13:31:47.069 UTC [33] STATEMENT:
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
IF EXISTS (
|
|
||||||
|
|
||||||
SELECT 1
|
|
||||||
|
|
||||||
FROM information_schema.columns
|
|
||||||
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
|
|
||||||
AND table_name = 'Users'
|
|
||||||
|
|
||||||
AND column_name = 'Id'
|
|
||||||
|
|
||||||
) THEN
|
|
||||||
|
|
||||||
EXECUTE 'CREATE SEQUENCE IF NOT EXISTS public."Users_Id_seq"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER SEQUENCE public."Users_Id_seq" OWNED BY public."Users"."Id"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''''public."Users_Id_seq"'''')';
|
|
||||||
|
|
||||||
EXECUTE 'SELECT setval(''''public."Users_Id_seq"'''', COALESCE((SELECT MAX("Id") FROM public."Users"), 0) + 1, false)';
|
|
||||||
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END $$
|
|
||||||
|
|
||||||
2026-06-26 13:31:47.407 UTC [33] ERROR: column "IsTenantAdmin" is of type integer but expression is of type boolean at character 103
|
|
||||||
|
|
||||||
2026-06-26 13:31:47.407 UTC [33] HINT: You will need to rewrite or cast the expression.
|
|
||||||
|
|
||||||
2026-06-26 13:31:47.407 UTC [33] STATEMENT: INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
|
|
||||||
RETURNING "Id"
|
|
||||||
|
|
||||||
2026-06-26 13:31:55.406 UTC [34] ERROR: syntax error at or near "public" at character 68
|
|
||||||
|
|
||||||
2026-06-26 13:31:55.406 UTC [34] QUERY: ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''public."Users_Id_seq"'')
|
|
||||||
|
|
||||||
2026-06-26 13:31:55.406 UTC [34] CONTEXT: PL/pgSQL function inline_code_block line 12 at EXECUTE
|
|
||||||
|
|
||||||
2026-06-26 13:31:55.406 UTC [34] STATEMENT:
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
IF EXISTS (
|
|
||||||
|
|
||||||
SELECT 1
|
|
||||||
|
|
||||||
FROM information_schema.columns
|
|
||||||
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
|
|
||||||
AND table_name = 'Users'
|
|
||||||
|
|
||||||
AND column_name = 'Id'
|
|
||||||
|
|
||||||
) THEN
|
|
||||||
|
|
||||||
EXECUTE 'CREATE SEQUENCE IF NOT EXISTS public."Users_Id_seq"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER SEQUENCE public."Users_Id_seq" OWNED BY public."Users"."Id"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''''public."Users_Id_seq"'''')';
|
|
||||||
|
|
||||||
EXECUTE 'SELECT setval(''''public."Users_Id_seq"'''', COALESCE((SELECT MAX("Id") FROM public."Users"), 0) + 1, false)';
|
|
||||||
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END $$
|
|
||||||
|
|
||||||
2026-06-26 13:31:55.737 UTC [34] ERROR: column "IsTenantAdmin" is of type integer but expression is of type boolean at character 103
|
|
||||||
|
|
||||||
2026-06-26 13:31:55.737 UTC [34] HINT: You will need to rewrite or cast the expression.
|
|
||||||
|
|
||||||
2026-06-26 13:31:55.737 UTC [34] STATEMENT: INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
|
|
||||||
RETURNING "Id"
|
|
||||||
|
|
||||||
2026-06-26 13:32:09.909 UTC [35] ERROR: syntax error at or near "public" at character 68
|
|
||||||
|
|
||||||
2026-06-26 13:32:09.909 UTC [35] QUERY: ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''public."Users_Id_seq"'')
|
|
||||||
|
|
||||||
2026-06-26 13:32:09.909 UTC [35] CONTEXT: PL/pgSQL function inline_code_block line 12 at EXECUTE
|
|
||||||
|
|
||||||
2026-06-26 13:32:09.909 UTC [35] STATEMENT:
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
IF EXISTS (
|
|
||||||
|
|
||||||
SELECT 1
|
|
||||||
|
|
||||||
FROM information_schema.columns
|
|
||||||
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
|
|
||||||
AND table_name = 'Users'
|
|
||||||
|
|
||||||
AND column_name = 'Id'
|
|
||||||
|
|
||||||
) THEN
|
|
||||||
|
|
||||||
EXECUTE 'CREATE SEQUENCE IF NOT EXISTS public."Users_Id_seq"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER SEQUENCE public."Users_Id_seq" OWNED BY public."Users"."Id"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''''public."Users_Id_seq"'''')';
|
|
||||||
|
|
||||||
EXECUTE 'SELECT setval(''''public."Users_Id_seq"'''', COALESCE((SELECT MAX("Id") FROM public."Users"), 0) + 1, false)';
|
|
||||||
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END $$
|
|
||||||
|
|
||||||
2026-06-26 13:32:10.239 UTC [35] ERROR: column "IsTenantAdmin" is of type integer but expression is of type boolean at character 103
|
|
||||||
|
|
||||||
2026-06-26 13:32:10.239 UTC [35] HINT: You will need to rewrite or cast the expression.
|
|
||||||
|
|
||||||
2026-06-26 13:32:10.239 UTC [35] STATEMENT: INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
|
|
||||||
RETURNING "Id"
|
|
||||||
|
|
||||||
2026-06-26 13:32:37.184 UTC [36] ERROR: syntax error at or near "public" at character 68
|
|
||||||
|
|
||||||
2026-06-26 13:32:37.184 UTC [36] QUERY: ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''public."Users_Id_seq"'')
|
|
||||||
|
|
||||||
2026-06-26 13:32:37.184 UTC [36] CONTEXT: PL/pgSQL function inline_code_block line 12 at EXECUTE
|
|
||||||
|
|
||||||
2026-06-26 13:32:37.184 UTC [36] STATEMENT:
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
|
|
||||||
BEGIN
|
|
||||||
|
|
||||||
IF EXISTS (
|
|
||||||
|
|
||||||
SELECT 1
|
|
||||||
|
|
||||||
FROM information_schema.columns
|
|
||||||
|
|
||||||
WHERE table_schema = 'public'
|
|
||||||
|
|
||||||
AND table_name = 'Users'
|
|
||||||
|
|
||||||
AND column_name = 'Id'
|
|
||||||
|
|
||||||
) THEN
|
|
||||||
|
|
||||||
EXECUTE 'CREATE SEQUENCE IF NOT EXISTS public."Users_Id_seq"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER SEQUENCE public."Users_Id_seq" OWNED BY public."Users"."Id"';
|
|
||||||
|
|
||||||
EXECUTE 'ALTER TABLE public."Users" ALTER COLUMN "Id" SET DEFAULT nextval(''''public."Users_Id_seq"'''')';
|
|
||||||
|
|
||||||
EXECUTE 'SELECT setval(''''public."Users_Id_seq"'''', COALESCE((SELECT MAX("Id") FROM public."Users"), 0) + 1, false)';
|
|
||||||
|
|
||||||
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
END $$
|
|
||||||
|
|
||||||
2026-06-26 13:32:37.515 UTC [36] ERROR: column "IsTenantAdmin" is of type integer but expression is of type boolean at character 103
|
|
||||||
|
|
||||||
2026-06-26 13:32:37.515 UTC [36] HINT: You will need to rewrite or cast the expression.
|
|
||||||
|
|
||||||
2026-06-26 13:32:37.515 UTC [36] STATEMENT: INSERT INTO "Users" ("IsTenantAdmin", "PasswordHash", "PasswordSalt", "TenantId", "Username")
|
|
||||||
|
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
|
|
||||||
RETURNING "Id"
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
Binary file not shown.
BIN
Binary file not shown.
+68
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+171
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
+14
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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
Reference in New Issue
Block a user