Compare commits
7 Commits
| 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
-18
@@ -1,9 +1,6 @@
|
||||
@page "/feiertage"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@attribute [Authorize]
|
||||
@inject IHolidayService HolidayService
|
||||
@inject ITimetrackerService TrackerService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@rendermode InteractiveServer
|
||||
@inject HolidayService HolidayService
|
||||
|
||||
<PageTitle>Feiertage – Timetracker</PageTitle>
|
||||
|
||||
@@ -20,7 +17,7 @@ else
|
||||
|
||||
@* ── Header ── *@
|
||||
<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">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||
Style="color:white" Size="Size.Large" OnClick="PrevYear" />
|
||||
@@ -140,7 +137,7 @@ else
|
||||
|
||||
@* ── Zusammenfassung ── *@
|
||||
<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" AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Celebration" Style="color:#00897B" />
|
||||
@@ -188,25 +185,15 @@ else
|
||||
private List<PublicHoliday> _holidays = [];
|
||||
private string _subLabel = "";
|
||||
|
||||
private int _userId;
|
||||
private AppSettings _settings = new();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var claim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
|
||||
if (claim != null)
|
||||
{
|
||||
_userId = int.Parse(claim.Value);
|
||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
||||
}
|
||||
await LoadHolidays();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task LoadHolidays()
|
||||
{
|
||||
_holidays = await HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
|
||||
_holidays = await HolidayService.GetHolidaysAsync(_year);
|
||||
var count = _holidays.Count;
|
||||
var past = _holidays.Count(h => h.Date < DateOnly.FromDateTime(DateTime.Today));
|
||||
_subLabel = count == 0
|
||||
@@ -1,11 +1,8 @@
|
||||
@page "/"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@attribute [Authorize]
|
||||
@inject ITimetrackerService TrackerService
|
||||
@inject IHolidayService HolidayService
|
||||
@page "/"
|
||||
@rendermode InteractiveServer
|
||||
@inject TimetrackerService TrackerService
|
||||
@inject HolidayService HolidayService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<PageTitle>KW @_kw – Wochenübersicht – Timetracker</PageTitle>
|
||||
|
||||
@@ -21,8 +18,8 @@ else
|
||||
<MudStack Spacing="3">
|
||||
|
||||
@* ── Wochen-Header ── *@
|
||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl onboarding-week-header"
|
||||
Style="background: #1E293B; color: white;">
|
||||
<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">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||
Style="color:white" Size="Size.Large" OnClick="PrevWeek" />
|
||||
@@ -94,9 +91,9 @@ else
|
||||
else
|
||||
{
|
||||
@* ── 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};")">
|
||||
<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>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudStack Spacing="0">
|
||||
@@ -270,8 +267,8 @@ else
|
||||
}
|
||||
|
||||
@* ── Wochensumme ── *@
|
||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl onboarding-week-summary"
|
||||
Style="background: #0F172A; color:white;">
|
||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||
Style="background: linear-gradient(135deg, #1A237E 0%, #283593 100%); color:white;">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mb-4">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Summarize" Style="color:rgba(255,255,255,0.8)" />
|
||||
<MudText Typo="Typo.h6" Style="color:white; font-weight:600">Wochensumme</MudText>
|
||||
@@ -307,7 +304,7 @@ else
|
||||
</MudPaper>
|
||||
|
||||
@* ── 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")};")">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Wrap="Wrap.Wrap" Spacing="3">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
@@ -329,18 +326,12 @@ else
|
||||
</MudPaper>
|
||||
|
||||
</MudStack>
|
||||
|
||||
@if (_showOnboarding)
|
||||
{
|
||||
<OnboardingTour OnFinished="HandleOnboardingFinished" />
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||
|
||||
private bool _loading = true;
|
||||
private int _userId;
|
||||
private DateOnly _monday;
|
||||
private List<DayVm> _days = [];
|
||||
private AppSettings _settings = new();
|
||||
@@ -353,72 +344,31 @@ else
|
||||
private TimeSpan _totalOvertime;
|
||||
private Dictionary<DateOnly, string> _holidays = [];
|
||||
private int _holidayYear = -1;
|
||||
private bool _showOnboarding;
|
||||
|
||||
private bool IsCurrentWeek => _monday == GetMonday(DateOnly.FromDateTime(DateTime.Today));
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
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));
|
||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
||||
|
||||
var loadWeekTask = LoadWeek();
|
||||
var overtimeTask = TrackerService.GetTotalOvertimeAsync(_userId, _settings);
|
||||
|
||||
await Task.WhenAll(loadWeekTask, overtimeTask);
|
||||
_totalOvertime = await overtimeTask;
|
||||
|
||||
try
|
||||
{
|
||||
var showOnb = await JSRuntime.InvokeAsync<string>("localStorage.getItem", "showOnboarding");
|
||||
_showOnboarding = showOnb == "true";
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignored during prerendering/SSR
|
||||
}
|
||||
|
||||
_settings = await TrackerService.GetSettingsAsync();
|
||||
await LoadWeek();
|
||||
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_settings);
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task HandleOnboardingFinished()
|
||||
{
|
||||
_showOnboarding = false;
|
||||
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "showOnboarding", "false");
|
||||
}
|
||||
|
||||
private async Task LoadWeek()
|
||||
{
|
||||
Task<List<PublicHoliday>> holidaysTask;
|
||||
if (_monday.Year != _holidayYear)
|
||||
{
|
||||
holidaysTask = HolidayService.GetHolidaysAsync(_monday.Year, _settings.GermanState);
|
||||
}
|
||||
else
|
||||
{
|
||||
holidaysTask = Task.FromResult(new List<PublicHoliday>());
|
||||
}
|
||||
|
||||
var dbDaysTask = TrackerService.GetWeekAsync(_userId, _monday);
|
||||
|
||||
await Task.WhenAll(holidaysTask, dbDaysTask);
|
||||
|
||||
if (_monday.Year != _holidayYear)
|
||||
{
|
||||
var list = await holidaysTask;
|
||||
var list = await HolidayService.GetHolidaysAsync(_monday.Year);
|
||||
_holidays = list.ToDictionary(h => h.Date, h => h.Name);
|
||||
_holidayYear = _monday.Year;
|
||||
}
|
||||
|
||||
var dbDays = await dbDaysTask;
|
||||
var dbDays = await TrackerService.GetWeekAsync(_monday);
|
||||
_days = Enumerable.Range(0, 7).Select(i =>
|
||||
{
|
||||
var date = _monday.AddDays(i);
|
||||
return DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date, _userId);
|
||||
return DayVm.From(dbDays.FirstOrDefault(d => d.Date == date), date);
|
||||
}).ToList();
|
||||
BuildWeekLabels();
|
||||
}
|
||||
@@ -471,7 +421,7 @@ else
|
||||
private async Task SaveDay(DayVm day)
|
||||
{
|
||||
await TrackerService.UpsertWorkDayAsync(day.ToWorkDay());
|
||||
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_userId, _settings);
|
||||
_totalOvertime = await TrackerService.GetTotalOvertimeAsync(_settings);
|
||||
BuildWeekLabels();
|
||||
}
|
||||
|
||||
@@ -536,7 +486,6 @@ else
|
||||
private sealed class DayVm
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public TimeSpan? Start { get; set; }
|
||||
public TimeSpan? End { get; set; }
|
||||
@@ -551,10 +500,9 @@ else
|
||||
|
||||
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,
|
||||
UserId = wd?.UserId ?? userId,
|
||||
Date = date,
|
||||
Start = wd?.StartTime?.ToTimeSpan(),
|
||||
End = wd?.EndTime?.ToTimeSpan(),
|
||||
@@ -569,7 +517,6 @@ else
|
||||
public WorkDay ToWorkDay() => new()
|
||||
{
|
||||
Id = Id,
|
||||
UserId = UserId,
|
||||
Date = Date,
|
||||
StartTime = Start.HasValue ? TimeOnly.FromTimeSpan(Start.Value) : null,
|
||||
EndTime = End.HasValue ? TimeOnly.FromTimeSpan(End.Value) : null,
|
||||
@@ -1,9 +1,7 @@
|
||||
@page "/month"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@attribute [Authorize]
|
||||
@inject ITimetrackerService TrackerService
|
||||
@inject IHolidayService HolidayService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@rendermode InteractiveServer
|
||||
@inject TimetrackerService TrackerService
|
||||
@inject HolidayService HolidayService
|
||||
|
||||
<PageTitle>@_deCulture.DateTimeFormat.GetMonthName(_month) @_year – Monatsübersicht – Timetracker</PageTitle>
|
||||
|
||||
@@ -20,7 +18,7 @@ else
|
||||
|
||||
@* ── Monats-Header ── *@
|
||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||
Style="background: #1E293B; color: white;">
|
||||
Style="background: linear-gradient(135deg, #3F51B5 0%, #1A237E 100%); color: white;">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||
Style="color:white" Size="Size.Large" OnClick="PrevMonth" />
|
||||
@@ -51,7 +49,7 @@ else
|
||||
<MudCardContent Class="pa-0">
|
||||
<MudSimpleTable Dense="true" Striped="false" Hover="true" Style="overflow-x:auto">
|
||||
<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">Start</th>
|
||||
<th style="font-weight:700; padding:10px 16px">Ende</th>
|
||||
@@ -95,7 +93,7 @@ else
|
||||
|
||||
@* ── Monatszusammenfassung ── *@
|
||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||
Style="background: linear-gradient(135deg, rgba(14,165,233,0.08) 0%, rgba(14,165,233,0.02) 100%); border-left: 6px solid #0EA5E9;">
|
||||
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">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CalendarViewMonth" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.h6" Style="font-weight:700">Monatszusammenfassung</MudText>
|
||||
@@ -152,7 +150,6 @@ else
|
||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||
|
||||
private bool _loading = true;
|
||||
private int _userId;
|
||||
private int _year = DateTime.Today.Year;
|
||||
private int _month = DateTime.Today.Month;
|
||||
private List<MonthDayVm> _days = [];
|
||||
@@ -170,26 +167,16 @@ else
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (claim == null) return;
|
||||
_userId = int.Parse(claim.Value);
|
||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
||||
_settings = await TrackerService.GetSettingsAsync();
|
||||
await LoadMonth();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task LoadMonth()
|
||||
{
|
||||
var workDaysTask = TrackerService.GetMonthAsync(_userId, _year, _month);
|
||||
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
|
||||
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year);
|
||||
|
||||
await Task.WhenAll(workDaysTask, holidaysTask, vacationsTask);
|
||||
|
||||
var workDays = await workDaysTask;
|
||||
var holidays = await holidaysTask;
|
||||
var vacations = await vacationsTask;
|
||||
var workDays = await TrackerService.GetMonthAsync(_year, _month);
|
||||
var holidays = await HolidayService.GetHolidaysAsync(_year);
|
||||
var vacations = await TrackerService.GetVacationDaysAsync(_year);
|
||||
|
||||
var holidayMap = holidays.ToDictionary(h => h.Date, h => h.Name);
|
||||
var vacationSet = vacations.Select(v => v.Date).ToHashSet();
|
||||
+14
-156
@@ -1,13 +1,8 @@
|
||||
@page "/settings"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@attribute [Authorize]
|
||||
@inject ITimetrackerService TrackerService
|
||||
@inject IHolidayService HolidayService
|
||||
@page "/settings"
|
||||
@rendermode InteractiveServer
|
||||
@inject TimetrackerService TrackerService
|
||||
@inject HolidayService HolidayService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject NavigationManager Nav
|
||||
|
||||
|
||||
<PageTitle>Einstellungen – Timetracker</PageTitle>
|
||||
|
||||
@@ -23,7 +18,7 @@ else
|
||||
|
||||
@* ── Header ── *@
|
||||
<MudPaper Elevation="4" Class="pa-5 rounded-xl"
|
||||
Style="background: #1E293B; color:white;">
|
||||
Style="background: linear-gradient(135deg, #3F51B5 0%, #1A237E 100%); color:white;">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Settings" Style="color:white; font-size:2rem" />
|
||||
<MudStack Spacing="0">
|
||||
@@ -131,92 +126,6 @@ else
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
@* ── Region & Feiertage ── *@
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard Elevation="3" Class="rounded-xl h-100">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Public" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.h6" Style="font-weight:600">Region & Feiertage</MudText>
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="4">
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
Wähle dein Bundesland aus, um Bundesland-spezifische Feiertage zu berücksichtigen.
|
||||
</MudText>
|
||||
<MudSelect T="string" Label="Bundesland" Variant="Variant.Outlined" @bind-Value="_settings.GermanState" Clearable="true" Placeholder="Nur bundesweite Feiertage">
|
||||
@foreach (var state in GermanStates)
|
||||
{
|
||||
<MudSelectItem Value="@state.Key">@state.Value</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
@* ── Gleitzeitkonto ── *@
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard Elevation="3" Class="rounded-xl h-100">
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountBalance" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.h6" Style="font-weight:600">Gleitzeitkonto-Start</MudText>
|
||||
</MudStack>
|
||||
</CardHeaderContent>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudStack Spacing="4">
|
||||
<MudDatePicker Label="Berechnungsstart"
|
||||
@bind-Date="_flexStartDate"
|
||||
HelperText="Gleitzeitberechnung läuft ab diesem Datum. Wenn leer, ab dem ersten Arbeitseintrag."
|
||||
Variant="Variant.Outlined"
|
||||
Clearable="true"
|
||||
DateFormat="dd.MM.yyyy"
|
||||
PickerVariant="PickerVariant.Inline" />
|
||||
|
||||
<MudNumericField @bind-Value="_settings.FlexTimeStartingBalanceHours"
|
||||
Label="Anfangsüberstunden (h)"
|
||||
Variant="Variant.Outlined"
|
||||
Step="0.5"
|
||||
Format="0.##"
|
||||
HelperText="Stufensaldo (Guthaben/Schulden) zum Berechnungsstart" />
|
||||
</MudStack>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
@* ── 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>
|
||||
|
||||
</MudGrid>
|
||||
|
||||
@* ── Speichern-Button ── *@
|
||||
@@ -466,33 +375,6 @@ else
|
||||
private static readonly System.Globalization.CultureInfo _deCulture = new("de-DE");
|
||||
|
||||
private AppSettings? _settings;
|
||||
private int _userId;
|
||||
private DateTime? _flexStartDate
|
||||
{
|
||||
get => _settings?.FlexTimeStartDate?.ToDateTime(TimeOnly.MinValue);
|
||||
set => _settings!.FlexTimeStartDate = value.HasValue ? DateOnly.FromDateTime(value.Value) : null;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> GermanStates = new()
|
||||
{
|
||||
{ "DE-BW", "Baden-Württemberg" },
|
||||
{ "DE-BY", "Bayern" },
|
||||
{ "DE-BE", "Berlin" },
|
||||
{ "DE-BB", "Brandenburg" },
|
||||
{ "DE-HB", "Bremen" },
|
||||
{ "DE-HH", "Hamburg" },
|
||||
{ "DE-HE", "Hessen" },
|
||||
{ "DE-MV", "Mecklenburg-Vorpommern" },
|
||||
{ "DE-NI", "Niedersachsen" },
|
||||
{ "DE-NW", "Nordrhein-Westfalen" },
|
||||
{ "DE-RP", "Rheinland-Pfalz" },
|
||||
{ "DE-SL", "Saarland" },
|
||||
{ "DE-SN", "Sachsen" },
|
||||
{ "DE-ST", "Sachsen-Anhalt" },
|
||||
{ "DE-SH", "Schleswig-Holstein" },
|
||||
{ "DE-TH", "Thüringen" }
|
||||
};
|
||||
|
||||
private int _vacYear = DateTime.Today.Year;
|
||||
private List<VacationDay> _vacationDays = [];
|
||||
private DateTime? _newVacDateFrom;
|
||||
@@ -520,22 +402,14 @@ else
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (claim == null) return;
|
||||
_userId = int.Parse(claim.Value);
|
||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
||||
|
||||
var loadVacationsTask = LoadVacations();
|
||||
var loadHolidaysTask = HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
|
||||
|
||||
await Task.WhenAll(loadVacationsTask, loadHolidaysTask);
|
||||
_holHolidays = await loadHolidaysTask;
|
||||
_settings = await TrackerService.GetSettingsAsync();
|
||||
await LoadVacations();
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||
}
|
||||
|
||||
private async Task LoadVacations()
|
||||
{
|
||||
_vacationDays = await TrackerService.GetVacationDaysAsync(_userId, _vacYear);
|
||||
_vacationDays = await TrackerService.GetVacationDaysAsync(_vacYear);
|
||||
}
|
||||
|
||||
private async Task ChangeYear(int delta)
|
||||
@@ -548,8 +422,6 @@ else
|
||||
{
|
||||
if (_settings == null) return;
|
||||
await TrackerService.SaveSettingsAsync(_settings);
|
||||
// Nach dem Speichern Feiertage neu laden, falls sich das Bundesland geändert hat
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings.GermanState);
|
||||
Snackbar.Add("Einstellungen gespeichert", Severity.Success);
|
||||
}
|
||||
|
||||
@@ -566,7 +438,7 @@ else
|
||||
{
|
||||
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++;
|
||||
}
|
||||
current = current.AddDays(1);
|
||||
@@ -580,7 +452,7 @@ else
|
||||
|
||||
private async Task RemoveVacation(int id)
|
||||
{
|
||||
await TrackerService.RemoveVacationDayAsync(_userId, id);
|
||||
await TrackerService.RemoveVacationDayAsync(id);
|
||||
await LoadVacations();
|
||||
Snackbar.Add("Urlaubstag entfernt", Severity.Info);
|
||||
}
|
||||
@@ -589,14 +461,14 @@ else
|
||||
private async Task ChangeHolYear(int delta)
|
||||
{
|
||||
_holYear += delta;
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||
}
|
||||
|
||||
private async Task FetchHolidays()
|
||||
{
|
||||
_fetchingHolidays = true;
|
||||
var (success, message) = await HolidayService.FetchAndStoreAsync(_holYear);
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||
_fetchingHolidays = false;
|
||||
Snackbar.Add(message, success ? Severity.Success : Severity.Error);
|
||||
}
|
||||
@@ -604,24 +476,10 @@ else
|
||||
private async Task DeleteHoliday(int id)
|
||||
{
|
||||
await HolidayService.DeleteAsync(id);
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear, _settings?.GermanState);
|
||||
_holHolidays = await HolidayService.GetHolidaysAsync(_holYear);
|
||||
Snackbar.Add("Feiertag entfernt", Severity.Info);
|
||||
}
|
||||
|
||||
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 static string FormatHours(double hours)
|
||||
{
|
||||
var ts = TimeSpan.FromHours(hours);
|
||||
+8
-20
@@ -1,10 +1,8 @@
|
||||
@page "/urlaub-maximizer"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@attribute [Authorize]
|
||||
@inject ITimetrackerService TrackerService
|
||||
@inject IHolidayService HolidayService
|
||||
@rendermode InteractiveServer
|
||||
@inject TimetrackerService TrackerService
|
||||
@inject HolidayService HolidayService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
|
||||
<PageTitle>Urlaubs-Maximizer – Timetracker</PageTitle>
|
||||
|
||||
@@ -21,7 +19,7 @@ else
|
||||
|
||||
@* ── Header ── *@
|
||||
<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">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
|
||||
Style="color:white" Size="Size.Large" OnClick="PrevYear" />
|
||||
@@ -234,28 +232,18 @@ else
|
||||
private int _remainingDays;
|
||||
private List<Suggestion> _suggestions = [];
|
||||
private string _subLabel = "";
|
||||
private int _userId;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier);
|
||||
if (claim == null) return;
|
||||
_userId = int.Parse(claim.Value);
|
||||
_settings = await TrackerService.GetSettingsAsync(_userId);
|
||||
_settings = await TrackerService.GetSettingsAsync();
|
||||
await LoadYear();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task LoadYear()
|
||||
{
|
||||
var holidaysTask = HolidayService.GetHolidaysAsync(_year, _settings.GermanState);
|
||||
var vacationsTask = TrackerService.GetVacationDaysAsync(_userId, _year);
|
||||
|
||||
await Task.WhenAll(holidaysTask, vacationsTask);
|
||||
|
||||
var holidays = await holidaysTask;
|
||||
var vacations = await vacationsTask;
|
||||
var holidays = await HolidayService.GetHolidaysAsync(_year);
|
||||
var vacations = await TrackerService.GetVacationDaysAsync(_year);
|
||||
_holidays = holidays.ToDictionary(h => h.Date, h => h.Name);
|
||||
_vacationSet = vacations.Select(v => v.Date).ToHashSet();
|
||||
_remainingDays = Math.Max(0, _settings.VacationDaysPerYear - vacations.Count);
|
||||
@@ -274,7 +262,7 @@ else
|
||||
private async Task TakeSuggestion(Suggestion s)
|
||||
{
|
||||
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();
|
||||
var word = s.VacationDaysNeeded == 1 ? "Urlaubstag" : "Urlaubstage";
|
||||
Snackbar.Add($"{s.VacationDaysNeeded} {word} eingetragen – {s.TotalFreeDays} Tage frei!", Severity.Success);
|
||||
@@ -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.Security.Claims
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@@ -10,8 +7,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using timetracker
|
||||
@using timetracker.Client.Components
|
||||
@using timetracker.Client.Components.Layout
|
||||
@using timetracker.Components
|
||||
@using timetracker.Components.Layout
|
||||
@using timetracker.Data
|
||||
@using timetracker.Shared
|
||||
@using MudBlazor
|
||||
@@ -1,15 +1,11 @@
|
||||
namespace timetracker.Shared;
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class AppSettings
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public double DailyTargetHours { get; set; } = 7.5;
|
||||
public int MinimumBreakMinutes { get; set; } = 30;
|
||||
public int VacationDaysPerYear { get; set; } = 30;
|
||||
public string? GermanState { get; set; }
|
||||
public DateOnly? FlexTimeStartDate { get; set; }
|
||||
public double FlexTimeStartingBalanceHours { get; set; } = 0.0;
|
||||
|
||||
// Arbeitstage
|
||||
public bool WorkMonday { get; set; } = true;
|
||||
@@ -1,10 +1,9 @@
|
||||
namespace timetracker.Shared;
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class BreakEntry
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int WorkDayId { get; set; }
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public WorkDay WorkDay { get; set; } = null!;
|
||||
public TimeOnly? StartTime { get; set; }
|
||||
public TimeOnly? EndTime { get; set; }
|
||||
@@ -1,30 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using timetracker.Shared;
|
||||
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, HttpClient http) : IHolidayService
|
||||
public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, HttpClient http)
|
||||
{
|
||||
private const string ApiUrl = "https://date.nager.at/api/v3/PublicHolidays/{0}/DE";
|
||||
|
||||
public async Task<List<PublicHoliday>> GetHolidaysAsync(int year, string? stateCode = null)
|
||||
public async Task<List<PublicHoliday>> GetHolidaysAsync(int year)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var holidays = await db.PublicHolidays
|
||||
return await db.PublicHolidays
|
||||
.Where(h => h.Date.Year == year)
|
||||
.OrderBy(h => h.Date)
|
||||
.ToListAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(stateCode))
|
||||
{
|
||||
// Default: return only global holidays (where Counties is null or empty)
|
||||
return holidays.Where(h => string.IsNullOrEmpty(h.Counties)).ToList();
|
||||
}
|
||||
|
||||
// Return global holidays OR holidays that match the user's state code
|
||||
return holidays.Where(h => string.IsNullOrEmpty(h.Counties) || h.Counties.Split(',').Contains(stateCode)).ToList();
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message)> FetchAndStoreAsync(int year)
|
||||
@@ -44,8 +34,7 @@ public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, Htt
|
||||
.Select(h => new PublicHoliday
|
||||
{
|
||||
Date = DateOnly.Parse(h.Date),
|
||||
Name = h.LocalName,
|
||||
Counties = h.Counties != null && h.Counties.Count > 0 ? string.Join(",", h.Counties) : null
|
||||
Name = h.LocalName
|
||||
}));
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
@@ -75,9 +64,5 @@ public class HolidayService(IDbContextFactory<TimetrackerDbContext> factory, Htt
|
||||
|
||||
[JsonPropertyName("localName")]
|
||||
public string LocalName { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("counties")]
|
||||
public List<string>? Counties { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
+18
-62
@@ -26,21 +26,9 @@ namespace timetracker.Data.Migrations
|
||||
b.Property<double>("DailyTargetHours")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<DateOnly?>("FlexTimeStartDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<double>("FlexTimeStartingBalanceHours")
|
||||
.HasColumnType("REAL");
|
||||
|
||||
b.Property<string>("GermanState")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("MinimumBreakMinutes")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("VacationDaysPerYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
@@ -70,6 +58,24 @@ namespace timetracker.Data.Migrations
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -92,50 +98,6 @@ namespace timetracker.Data.Migrations
|
||||
b.ToTable("BreakEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("timetracker.Data.PublicHoliday", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Counties")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
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 =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -148,9 +110,6 @@ namespace timetracker.Data.Migrations
|
||||
b.Property<string>("Note")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("VacationDays");
|
||||
@@ -171,9 +130,6 @@ namespace timetracker.Data.Migrations
|
||||
b.Property<TimeOnly?>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("WorkDays");
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace timetracker.Shared;
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class PublicHoliday
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public string Name { get; set; } = "";
|
||||
public string? Counties { get; set; }
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using timetracker.Shared;
|
||||
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class TimetrackerDbContext(DbContextOptions<TimetrackerDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<WorkDay> WorkDays => Set<WorkDay>();
|
||||
public DbSet<BreakEntry> BreakEntries => Set<BreakEntry>();
|
||||
public DbSet<AppSettings> AppSettings => Set<AppSettings>();
|
||||
@@ -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,9 +1,8 @@
|
||||
namespace timetracker.Shared;
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class VacationDay
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
namespace timetracker.Shared;
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class WorkDay
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int UserId { get; set; }
|
||||
public DateOnly Date { get; set; }
|
||||
public TimeOnly? StartTime { get; set; }
|
||||
public TimeOnly? EndTime { get; set; }
|
||||
@@ -2,35 +2,23 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy project files for restoring dependencies
|
||||
COPY timetracker.slnx ./
|
||||
COPY timetracker.Server/timetracker.Server.csproj timetracker.Server/
|
||||
COPY timetracker.Client/timetracker.Client.csproj timetracker.Client/
|
||||
COPY timetracker.Shared/timetracker.Shared.csproj timetracker.Shared/
|
||||
# Keine Unterordner mehr beim Kopieren!
|
||||
COPY timetracker.csproj ./
|
||||
RUN dotnet restore timetracker.csproj
|
||||
|
||||
# Restore dependencies
|
||||
RUN dotnet restore timetracker.slnx
|
||||
|
||||
# Copy the rest of the source code
|
||||
COPY timetracker.Server/ timetracker.Server/
|
||||
COPY timetracker.Client/ timetracker.Client/
|
||||
COPY timetracker.Shared/ timetracker.Shared/
|
||||
|
||||
# Publish
|
||||
WORKDIR /src/timetracker.Server
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
COPY . ./
|
||||
RUN dotnet publish -c Release -o /app/publish --no-restore
|
||||
|
||||
# ── Runtime Stage ─────────────────────────────────────────────────────────────
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# Directory for SQLite database
|
||||
# Verzeichnis für die SQLite-Datenbank
|
||||
RUN mkdir -p /data
|
||||
|
||||
ENV ASPNETCORE_HTTP_PORTS=8080
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
||||
ENV TIMETRACKER_DB_PATH=/data/timetracker.db
|
||||
ENV EnableHttpsRedirect=false
|
||||
|
||||
@@ -38,4 +26,4 @@ EXPOSE 8080
|
||||
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["dotnet", "timetracker.Server.dll"]
|
||||
ENTRYPOINT ["dotnet", "timetracker.dll"]
|
||||
+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();
|
||||
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.
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