Files

341 lines
20 KiB
Plaintext

@page "/"
@using MudBlazor
@inject ISnackbar Snackbar
<PageTitle>Dashboard | OnProf</PageTitle>
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-8">
<MudGrid>
<MudItem xs="12">
<div class="d-flex align-center">
<MudText Typo="Typo.h4" Class="fw-bold">Hallo, Marc!</MudText>
<MudChip T="string" Color="Color.Primary" Variant="Variant.Text" Class="ml-4">März 2026</MudChip>
</div>
<MudText Typo="Typo.body1" Color="Color.Secondary">Hier ist deine Übersicht für die aktuelle Woche.</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4" Style="height: 120px; border-left: 6px solid var(--mud-palette-primary);">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Heute gebucht</MudText>
<div class="d-flex align-end justify-space-between">
<MudText Typo="Typo.h3">6,5 <small style="font-size: 1rem">h</small></MudText>
<MudIcon Icon="@Icons.Material.Filled.HistoryToggleOff" Color="Color.Primary" Size="Size.Large" />
</div>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4" Style="height: 120px; border-left: 6px solid var(--mud-palette-success);">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Wochenfortschritt</MudText>
<MudText Typo="Typo.h3">32 <small style="font-size: 1rem">/ 40h</small></MudText>
<MudProgressLinear Color="Color.Success" Value="80" Class="mt-2" />
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4" Style="height: 120px; border-left: 6px solid var(--mud-palette-info);">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Aktuelles Projekt</MudText>
<MudText Typo="Typo.h6" Class="mt-2" Style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
00000001 - Gleitzeit
</MudText>
<MudLink Typo="Typo.caption" Color="Color.Info">Projekt wechseln</MudLink>
</MudPaper>
</MudItem>
<MudItem xs="12" sm="6" md="3">
<MudPaper Elevation="2" Class="pa-4" Style="height: 120px; border-left: 6px solid var(--mud-palette-warning);">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Urlaub / Überstunden</MudText>
<div class="d-flex gap-4 mt-2">
<div>
<MudText Typo="Typo.h6">12</MudText>
<MudText Typo="Typo.caption">Urlaub</MudText>
</div>
<MudDivider Vertical="true" FlexItem="true" />
<div>
<MudText Typo="Typo.h6">+14,5</MudText>
<MudText Typo="Typo.caption">Gleitzeit</MudText>
</div>
</div>
</MudPaper>
</MudItem>
<MudItem xs="12">
<MudTabs Elevation="2" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-6" Color="Color.Primary">
<MudTabPanel Icon="@Icons.Material.Filled.ViewWeek" Text="Wochenansicht">
<MudDropContainer @ref="_dropContainer" T="DropItem" Items="_items"
ItemsSelector="@((item, dropzone) => item.Status == dropzone && (dropzone != "Backlog" || string.IsNullOrWhiteSpace(_backlogSearchString) || item.Project.Contains(_backlogSearchString, StringComparison.OrdinalIgnoreCase) || item.Task.Contains(_backlogSearchString, StringComparison.OrdinalIgnoreCase)))"
ItemDropped="ItemUpdated" Class="d-flex flex-column flex-grow-1">
<ChildContent>
<div class="d-flex justify-space-between align-center mb-6">
<MudButtonGroup Color="Color.Default" Variant="Variant.Outlined">
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft" />
<MudButton>KW 12</MudButton>
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight" />
</MudButtonGroup>
</div>
<MudGrid>
<MudItem xs="12" md="9">
<div class="d-flex gap-2 overflow-x-auto pb-4">
@foreach (var day in _weekDays)
{
<div class="flex-1" style="min-width: 180px;">
<MudPaper Elevation="0" Class="pa-3 mb-2" Style="@(day.IsToday ? "background: var(--mud-palette-primary-hover);" : "background: transparent;")">
<MudText Typo="Typo.subtitle2" Align="Align.Center"><b>@day.Name</b></MudText>
<MudText Typo="Typo.caption" Align="Align.Center" Class="d-block">@day.Date.ToShortDateString()</MudText>
</MudPaper>
<MudDropZone T="DropItem" Identifier="@day.Name" Class="rounded-lg d-flex flex-column gap-2"
Style="min-height: 400px; border: 2px dashed var(--mud-palette-divider); padding: 8px;">
</MudDropZone>
<div class="mt-2 d-flex justify-center">
<MudText Typo="Typo.caption" Color="Color.Secondary">Summe: @(_items.Where(i => i.Status == day.Name).Sum(i => i.Hours))h</MudText>
</div>
</div>
}
</div>
</MudItem>
<MudItem xs="12" md="3">
<MudPaper Elevation="0" Class="pa-4 rounded-lg d-flex flex-column" Style="border: 2px solid var(--mud-palette-divider); height: 100%; min-height: 450px; background-color: var(--mud-palette-background-grey);">
<MudText Typo="Typo.h6" Class="mb-4">Projekt-Stapel</MudText>
<MudTextField T="string" Value="@_backlogSearchString" ValueChanged="OnBacklogSearchChanged"
Placeholder="Suchen..." Adornment="Adornment.Start" Immediate="true"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Small"
Class="mb-4 mt-0 bg-white" Margin="Margin.Dense" Variant="Variant.Outlined"></MudTextField>
<div class="flex-grow-1 overflow-y-auto pr-2" style="max-height: 400px;">
<MudDropZone T="DropItem" Identifier="Backlog" Class="d-flex flex-column gap-3" Style="min-height: 100%" />
</div>
</MudPaper>
</MudItem>
</MudGrid>
</ChildContent>
<ItemRenderer>
<MudPaper Elevation="2" Class="pa-3 rounded-lg border-l-4" Style="@($"border-left: 6px solid {context.Color}; cursor: grab; width: 100%; position: relative;")">
<div class="d-flex flex-column">
<div class="d-flex justify-space-between align-start">
<MudText Typo="Typo.caption" Class="mud-text-secondary" Style="font-size: 0.7rem">@context.Project</MudText>
@if (context.Status != "Backlog")
{
<MudIconButton Icon="@Icons.Material.Filled.Delete" Size="Size.Small" Color="Color.Error"
Style="position: absolute; top: 0px; right: 0px; padding: 4px;"
OnClick="() => DeleteItem(context)" />
}
</div>
<MudText Typo="Typo.body2" Class="mt-1"><b>@context.Task</b></MudText>
<div class="d-flex align-center justify-space-between mt-1">
<MudNumericField @bind-Value="context.Hours" Variant="Variant.Text" Margin="Margin.Dense" Style="width: 50px;" T="double" Step="0.5" Min="0" />
<MudIcon Icon="@Icons.Material.Filled.DragIndicator" Size="Size.Small" Color="Color.Error" />
</div>
</div>
</MudPaper>
</ItemRenderer>
</MudDropContainer>
</MudTabPanel>
<MudTabPanel Icon="@Icons.Material.Filled.List" Text="Alle Buchungen">
<MudPaper Elevation="0" Class="pa-4 mt-4" Style="border: 1px solid var(--mud-palette-divider); border-radius: 8px;">
<MudDataGrid T="BookingPlaceholder" Items="@_dummyBookings" Hover="true" Elevation="0" Striped="true" Dense="true" QuickFilter="@_quickFilter">
<ToolBarContent>
<MudText Typo="Typo.h6" Color="Color.Primary" Class="fw-bold mb-2"><b>Buchungsverlauf</b></MudText>
<MudSpacer />
<MudTextField @bind-Value="_searchString" Placeholder="Suchen..." Adornment="Adornment.Start" Immediate="true"
AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"
Variant="Variant.Outlined" Margin="Margin.Dense" Style="max-width: 300px;"></MudTextField>
</ToolBarContent>
<Columns>
<TemplateColumn T="BookingPlaceholder" Title="Datum" SortBy="@(x => x.Date)">
<CellTemplate>
<div class="d-flex align-center">
<MudIcon Icon="@Icons.Material.Filled.CalendarMonth" Size="Size.Small" Class="mr-2 mud-text-secondary" />
<MudText Typo="Typo.body2"><b>@context.Item.Date.ToString("dd.MM.yyyy")</b></MudText>
</div>
</CellTemplate>
</TemplateColumn>
<TemplateColumn T="BookingPlaceholder" Title="Auftrag" SortBy="@(x => x.Project)">
<CellTemplate>
<MudChip T="string" Color="Color.Primary" Size="Size.Small" Variant="Variant.Text">@context.Item.Project</MudChip>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="x => x.Task" Title="Tätigkeit" />
<TemplateColumn T="BookingPlaceholder" Title="Ansprechpartner" SortBy="@(x => x.ContactPerson)">
<CellTemplate>
<div class="d-flex align-center">
<MudAvatar Size="Size.Small" Color="Color.Secondary" Class="mr-2" Style="width: 24px; height: 24px; font-size: 0.75rem;">
@(string.IsNullOrWhiteSpace(context.Item.ContactPerson) ? "?" : context.Item.ContactPerson[0].ToString().ToUpper())
</MudAvatar>
<MudText Typo="Typo.body2">@context.Item.ContactPerson</MudText>
</div>
</CellTemplate>
</TemplateColumn>
<TemplateColumn T="BookingPlaceholder" Title="Stunden" SortBy="@(x => x.Hours)">
<CellTemplate>
<div class="d-flex align-center">
<MudText Typo="Typo.body2"><b>@context.Item.Hours</b> <small class="mud-text-secondary ml-1">h</small></MudText>
</div>
</CellTemplate>
</TemplateColumn>
<TemplateColumn T="BookingPlaceholder" CellClass="d-flex justify-end">
<CellTemplate>
<MudTooltip Text="Bearbeiten" Delay="400">
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Edit" Color="Color.Primary" Class="mr-2" />
</MudTooltip>
<MudTooltip Text="Löschen" Delay="400">
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Delete" Color="Color.Error" />
</MudTooltip>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="BookingPlaceholder" RowsPerPageString="Einträge pro Seite:" />
</PagerContent>
</MudDataGrid>
</MudPaper>
</MudTabPanel>
</MudTabs>
</MudItem>
</MudGrid>
</MudContainer>
<MudFab Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" Style="position: fixed; bottom: 24px; right: 24px;" OnClick="OpenBookingDialog" />
@code {
private MudDropContainer<DropItem> _dropContainer;
private string _searchString = "";
private string _backlogSearchString = "";
private List<DayInfo> _weekDays = new();
private List<DropItem> _items = new();
// Datenstrukturen
public class DropItem
{
public string Project { get; set; } = string.Empty;
public string Task { get; set; } = string.Empty;
public double Hours { get; set; }
public string Color { get; set; } = "#7e6fff";
public string Status { get; set; } = "Backlog";
}
public record DayInfo(string Name, DateTime Date, bool IsToday);
public record BookingPlaceholder(DateTime Date, string Project, string Task, double Hours, string ContactPerson);
// Dummy Daten für das DataGrid (Monatsansicht)
private List<BookingPlaceholder> _dummyBookings = new();
protected override void OnInitialized()
{
// Wochentage generieren (Mo-Fr)
var startOfWeek = DateTime.Now.AddDays(-(int)DateTime.Now.DayOfWeek + (int)DayOfWeek.Monday);
for (int i = 0; i < 5; i++)
{
var date = startOfWeek.AddDays(i);
_weekDays.Add(new DayInfo(date.ToString("dddd"), date, date.Date == DateTime.Today));
}
// Stapel an Vorlagen im Backlog
_items.Add(new DropItem { Project = "00001", Task = "Gleitzeit", Hours = 0, Color = "#7e6fff", Status = "Backlog" });
_items.Add(new DropItem { Project = "00010", Task = "Meeting", Hours = 0, Color = "#3dcb6c", Status = "Backlog" });
_items.Add(new DropItem { Project = "00500", Task = "Entwicklung", Hours = 0, Color = "#ffb545", Status = "Backlog" });
// Generiere Beispieldaten für den gesamten aktuellen Monat
GenerateMonthlyDummyData();
}
private void GenerateMonthlyDummyData()
{
var year = DateTime.Now.Year;
var month = DateTime.Now.Month;
var daysInMonth = DateTime.DaysInMonth(year, month);
var random = new Random(42); // Fester Seed für konstante Dummy-Daten
string[] projects = { "00001 - Gleitzeit", "00010 - Allg. Besprechung", "00500 - Webentwicklung", "01234 - Fehlerbehebung", "09876 - Konzeptphase" };
string[] tasks = { "Projektarbeit", "Meeting", "Jour Fixe", "Bugfixing", "Code Review", "Dokumentation" };
string[] contacts = { "Max Mustermann", "Anna Schmidt", "John Doe", "Maria Mayer", "Peter Parker" };
for (int day = 1; day <= daysInMonth; day++)
{
var currentDate = new DateTime(year, month, day);
// Keine Buchungen am Wochenende als Standard-Dummy
if (currentDate.DayOfWeek == DayOfWeek.Saturday || currentDate.DayOfWeek == DayOfWeek.Sunday)
continue;
// 1 bis 3 Einträge pro Tag
int entriesCount = random.Next(1, 4);
for (int i = 0; i < entriesCount; i++)
{
var hours = Math.Round(random.NextDouble() * 4 + 1, 1); // 1.0 bis 5.0 Stunden
// Runden auf halbe Stunden
hours = Math.Round(hours * 2, MidpointRounding.AwayFromZero) / 2;
_dummyBookings.Add(new BookingPlaceholder(
currentDate,
projects[random.Next(projects.Length)],
tasks[random.Next(tasks.Length)],
hours,
contacts[random.Next(contacts.Length)]
));
}
}
// Sortiere absteigend nach Datum
_dummyBookings = _dummyBookings.OrderByDescending(b => b.Date).ToList();
}
private void OnBacklogSearchChanged(string newValue)
{
_backlogSearchString = newValue;
_dropContainer.Refresh();
}
private void ItemUpdated(MudItemDropInfo<DropItem> dropInfo)
{
if (dropInfo.DropzoneIdentifier != "Backlog" && dropInfo.Item.Status == "Backlog")
{
// Erstelle eine echte Kopie vom Template und füge sie sofort hinzu
var newItem = new DropItem
{
Project = dropInfo.Item.Project,
Task = dropInfo.Item.Task,
Hours = dropInfo.Item.Hours,
Color = dropInfo.Item.Color,
Status = dropInfo.DropzoneIdentifier
};
_items.Add(newItem);
Snackbar.Add($"{newItem.Task} zum {dropInfo.DropzoneIdentifier} hinzugefügt", Severity.Success);
}
else
{
// Einfaches Verschieben zwischen den Tagen
dropInfo.Item.Status = dropInfo.DropzoneIdentifier;
}
_dropContainer.Refresh();
}
private void DeleteItem(DropItem item)
{
if (item.Status != "Backlog")
{
_items.Remove(item);
Snackbar.Add($"{item.Task} gelöscht", Severity.Warning);
_dropContainer.Refresh();
}
}
private Func<BookingPlaceholder, bool> _quickFilter => x =>
{
if (string.IsNullOrWhiteSpace(_searchString)) return true;
return x.Project.Contains(_searchString, StringComparison.OrdinalIgnoreCase) ||
x.Task.Contains(_searchString, StringComparison.OrdinalIgnoreCase);
};
private void OpenBookingDialog()
{
Snackbar.Add("Dialog für manuelle Buchung wird implementiert...", Severity.Info);
}
}