Buchungsseite + Fancy Kalender

This commit is contained in:
Marc Wieland
2025-10-17 10:41:53 +02:00
parent de2c369350
commit 059218eb19
77 changed files with 1725 additions and 55 deletions

View File

@@ -0,0 +1,139 @@
@page "/booking-entry"
@using OnProfNext.Client.Services
@using OnProfNext.Shared.Models.DTOs
@inject BookingApiService BookingService
@inject OrderApiService OrderService
@inject NavigationManager Navigation
<h3 class="mb-4 d-flex justify-content-between align-items-center">
<span class="text-primary">Zeit buchen</span>
<button class="btn btn-outline-secondary" @onclick="GoBack">
<i class="bi bi-arrow-left"></i> Zurück
</button>
</h3>
@if (isLoading)
{
<div class="text-center mt-5">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-3 text-secondary">Daten werden geladen...</p>
</div>
}
else if (errorMessage is not null)
{
<div class="alert alert-danger">@errorMessage</div>
}
else
{
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<button class="nav-link @(activeView == ViewType.Classic ? "active" : "")"
@onclick="() => activeView = ViewType.Classic">
<i class="bi bi-calendar-month me-1"></i> Klassische Ansicht
</button>
</li>
<li class="nav-item">
<button class="nav-link @(activeView == ViewType.Fancy ? "active" : "")"
@onclick="() => activeView = ViewType.Fancy">
<i class="bi bi-calendar-week me-1"></i> Fancy Ansicht
</button>
</li>
</ul>
<div class="tab-content p-3 border rounded bg-white shadow-sm">
@if (activeView == ViewType.Classic)
{
<ClassicView Bookings="MyBookings" Orders="AvailableOrders"
OnBookingCreated="ReloadBookings" OnError="ShowError" />
}
else
{
<FancyView Bookings="MyBookings" Orders="AvailableOrders"
OnBookingCreated="ReloadBookings" OnError="ShowError" />
}
</div>
}
@code {
private enum ViewType { Classic, Fancy }
private ViewType activeView = ViewType.Classic;
private bool isLoading = true;
private string? errorMessage;
private List<BookingDto> MyBookings = new();
private List<OrderDto> AvailableOrders = new();
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
isLoading = false;
}
private async Task LoadDataAsync()
{
try
{
// Lade Bookings des Users
var (bookingSuccess, bookings, bookingError) = await BookingService.GetMyBookingsAsync();
if (!bookingSuccess)
{
errorMessage = bookingError;
return;
}
MyBookings = bookings ?? new();
// Lade Orders des Users
var (orderSuccess, orders, orderError) = await OrderService.GetMyOrdersAsync();
if (!orderSuccess)
{
errorMessage = orderError;
return;
}
AvailableOrders = orders ?? new();
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Laden der Daten: {ex.Message}";
}
}
private async Task ReloadBookings()
{
var (success, bookings, error) = await BookingService.GetMyBookingsAsync();
if (success)
{
MyBookings = bookings ?? new();
StateHasChanged();
}
else
{
errorMessage = error;
}
}
private void ShowError(string error)
{
errorMessage = error;
StateHasChanged();
}
private void GoBack() => Navigation.NavigateTo("/projects");
}
<style>
.nav-tabs .nav-link {
color: #495057;
border: none;
border-bottom: 3px solid transparent;
}
.nav-tabs .nav-link.active {
color: #0d6efd;
font-weight: 600;
border-color: #0d6efd;
background-color: transparent;
}
.tab-content {
border-radius: 0.5rem;
}
</style>

View File

@@ -0,0 +1,209 @@
@using OnProfNext.Client.Services
@using OnProfNext.Shared.Models.DTOs
@inject BookingApiService BookingService
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<button class="btn btn-outline-primary" @onclick="PreviousMonth">
<i class="bi bi-chevron-left"></i> Vorheriger Monat
</button>
<h5 class="text-primary m-0">@CurrentMonth.ToString("MMMM yyyy")</h5>
<button class="btn btn-outline-primary" @onclick="NextMonth">
Nächster Monat <i class="bi bi-chevron-right"></i>
</button>
</div>
<div class="monthly-calendar">
<table class="table table-bordered text-center">
<thead class="table-light">
<tr>
<th>Mo</th>
<th>Di</th>
<th>Mi</th>
<th>Do</th>
<th>Fr</th>
<th>Sa</th>
<th>So</th>
</tr>
</thead>
<tbody>
@foreach (var week in GetCalendarWeeks())
{
<tr>
@foreach (var day in week)
{
<td class="@(day.Month == CurrentMonth.Month ? "" : "text-muted") @(IsWeekend(day) ? "weekend" : "")"
@onclick="() => OpenModal(day)">
@day.Day
<br />
<small class="@GetHoursColorClass(day)">@GetHoursForDay(day) h</small>
</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
@if (showModal)
{
<div class="modal fade show d-block" tabindex="-1" style="background-color: rgba(0,0,0,0.5);">
<div class="modal-dialog modal-dialog-centered" @onclick:stopPropagation>
<div class="modal-content border-0 shadow">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">Buchung für @selectedDate.ToString("dd.MM.yyyy")</h5>
<button type="button" class="btn-close btn-close-white" @onclick="CloseModal"></button>
</div>
<div class="modal-body">
<EditForm Model="newBooking" OnValidSubmit="SaveBookingAsync">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label class="form-label">Auftrag</label>
<InputSelect class="form-select" @bind-Value="newBooking.OrderId">
<option value="0" disabled>Auswählen...</option>
@foreach (var order in Orders)
{
<option value="@order.Id">@order.Titel (@order.Auftragsnummer)</option>
}
</InputSelect>
</div>
<div class="mb-3">
<label class="form-label">Stunden</label>
<InputNumber class="form-control" @bind-Value="newBooking.Hours" />
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<InputTextArea class="form-control" @bind-Value="newBooking.Description" />
</div>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-secondary me-2" @onclick="CloseModal">Abbrechen</button>
<button type="submit" class="btn btn-success">Speichern</button>
</div>
</EditForm>
</div>
</div>
</div>
</div>
}
@code {
[Parameter] public List<BookingDto> Bookings { get; set; } = new();
[Parameter] public List<OrderDto> Orders { get; set; } = new();
[Parameter] public EventCallback OnBookingCreated { get; set; }
[Parameter] public EventCallback<string> OnError { get; set; }
private DateTime CurrentMonth = DateTime.Today;
private bool showModal;
private DateTime selectedDate;
private BookingCreateDto newBooking = new();
private void PreviousMonth()
{
CurrentMonth = CurrentMonth.AddMonths(-1);
}
private void NextMonth()
{
CurrentMonth = CurrentMonth.AddMonths(1);
}
private List<DateTime[]> GetCalendarWeeks()
{
var weeks = new List<DateTime[]>();
var firstDayOfMonth = new DateTime(CurrentMonth.Year, CurrentMonth.Month, 1);
var firstMonday = firstDayOfMonth.AddDays(-(int)firstDayOfMonth.DayOfWeek + (firstDayOfMonth.DayOfWeek == DayOfWeek.Sunday ? 0 : 1));
var currentDay = firstMonday;
for (int weekCount = 0; weekCount < 6; weekCount++)
{
// Prüfe, ob currentDay noch im gültigen DateTime-Bereich liegt
if (currentDay.Year >= DateTime.MaxValue.Year)
break;
var week = new DateTime[7];
for (int i = 0; i < 7; i++)
{
week[i] = currentDay;
// Prüfe, ob das Hinzufügen eines Tages möglich ist
if (currentDay < DateTime.MaxValue.AddDays(-1))
currentDay = currentDay.AddDays(1);
else
break;
}
weeks.Add(week);
// Wenn currentDay nicht mehr incrementiert werden kann, beende die Schleife
if (currentDay >= DateTime.MaxValue.AddDays(-1))
break;
}
return weeks;
}
private bool IsWeekend(DateTime day)
{
return day.DayOfWeek == DayOfWeek.Saturday || day.DayOfWeek == DayOfWeek.Sunday;
}
private decimal GetHoursForDay(DateTime day)
{
return Bookings.Where(b => b.Date.Date == day.Date).Sum(b => b.Hours);
}
private string GetHoursColorClass(DateTime day)
{
var hours = GetHoursForDay(day);
if (hours >= 8)
return "text-success";
else if (hours > 6 && hours < 8)
return "text-warning";
else
return "text-danger";
}
private void OpenModal(DateTime date)
{
selectedDate = date;
newBooking = new() { Date = date, MandantId = 1 };
showModal = true;
}
private void CloseModal() => showModal = false;
private async Task SaveBookingAsync()
{
var (success, error) = await BookingService.CreateBookingAsync(newBooking);
if (success)
{
await OnBookingCreated.InvokeAsync();
CloseModal();
}
else
{
await OnError.InvokeAsync(error ?? "Fehler beim Speichern der Buchung.");
}
}
}
<style>
.monthly-calendar td {
cursor: pointer;
padding: 10px;
vertical-align: top;
height: 80px;
}
.monthly-calendar td:hover {
background-color: #f8f9fa;
}
.monthly-calendar td.weekend {
background-color: #f8f9fa;
}
</style>

View File

@@ -0,0 +1,282 @@
@using OnProfNext.Client.Services
@using OnProfNext.Shared.Models.DTOs
@inject BookingApiService BookingService
@inject IJSRuntime JSRuntime
<div class="fancy-layout d-flex">
<div class="orders-sidebar card shadow-sm border-0 me-3" style="width: 250px;">
<div class="card-body">
<h5 class="text-primary mb-3">Verfügbare Aufträge</h5>
<div class="list-group">
@if (Orders == null || !Orders.Any())
{
<p class="text-muted">Keine Aufträge verfügbar</p>
}
else
{
@foreach (var order in Orders)
{
<div class="list-group-item list-group-item-action draggable-order"
draggable="true" @ondragstart="() => StartDrag(order.Id)">
@order.Titel (@order.Auftragsnummer)
</div>
}
}
</div>
</div>
</div>
<div class="week-calendar flex-grow-1">
<div class="d-flex justify-content-between align-items-center mb-3">
<button class="btn btn-outline-primary" @onclick="PreviousWeek">
<i class="bi bi-chevron-left"></i> Vorherige Woche
</button>
<h5 class="text-primary m-0">
Woche vom @CurrentWeekStart.ToString("dd.MM.yyyy") bis @CurrentWeekStart.AddDays(6).ToString("dd.MM.yyyy")
</h5>
<button class="btn btn-outline-primary" @onclick="NextWeek">
Nächste Woche <i class="bi bi-chevron-right"></i>
</button>
</div>
<table class="table table-bordered text-center">
<thead class="table-light">
<tr>
<th style="width: 80px;">Zeit</th>
@foreach (var day in WeekDays)
{
<th class="@(IsWeekend(day) ? "weekend" : "")">
@day.ToString("dd.MM") (@day.ToString("ddd"))
<br />
<small class="@GetHoursColorClass(day)">@GetHoursForDay(day) h</small>
</th>
}
</tr>
</thead>
<tbody>
@for (int hour = 8; hour <= 18; hour++)
{
var localHour = hour;
<tr>
<td>@localHour:00</td>
@foreach (var day in WeekDays)
{
var localDay = day;
<td class="@(IsWeekend(localDay) ? "weekend" : "")"
@ondrop="() => DropOnSlot(localDay, localHour)" @ondragover="AllowDrop"
style="height: 50px; position: relative;">
@foreach (var booking in GetBookingsForSlot(localDay, localHour))
{
<div class="booking-item bg-primary text-white p-1"
style="position: absolute; top: 0; left: 0; right: 0; height: @(booking.Hours * 50)px;"
title="@booking.OrderTitle: @booking.Hours h">
@booking.OrderTitle - @booking.Hours h
<button class="btn btn-sm btn-danger float-end"
@onclick="() => DeleteBooking(booking.Id)">
<i class="bi bi-trash"></i>
</button>
</div>
}
</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>
@code {
[Parameter] public List<BookingDto>? Bookings { get; set; }
[Parameter] public List<OrderDto>? Orders { get; set; }
[Parameter] public EventCallback OnBookingCreated { get; set; }
[Parameter] public EventCallback<string> OnError { get; set; }
private DateTime CurrentWeekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek + 1);
private List<DateTime> WeekDays => Enumerable.Range(0, 7).Select(d => CurrentWeekStart.AddDays(d)).ToList();
private int? draggedOrderId;
private void PreviousWeek()
{
if (CurrentWeekStart.Year > DateTime.MinValue.Year)
{
CurrentWeekStart = CurrentWeekStart.AddDays(-7);
StateHasChanged();
}
}
private void NextWeek()
{
if (CurrentWeekStart.Year < DateTime.MaxValue.Year)
{
CurrentWeekStart = CurrentWeekStart.AddDays(7);
StateHasChanged();
}
}
private void StartDrag(int orderId)
{
draggedOrderId = orderId;
Console.WriteLine($"Start drag for Order ID: {orderId}");
}
private async Task AllowDrop()
{
await JSRuntime.InvokeVoidAsync("onProfNext.preventDefault");
}
private async Task DropOnSlot(DateTime day, int hour)
{
if (draggedOrderId == null) return;
var bookingDate = new DateTime(day.Year, day.Month, day.Day, hour, 0, 0, DateTimeKind.Local);
Console.WriteLine($"Calculated booking date: {bookingDate.ToString("dd.MM.yyyy HH:mm")} (Kind: {bookingDate.Kind})");
var newBooking = new BookingCreateDto
{
OrderId = draggedOrderId.Value,
Date = bookingDate,
Hours = 1,
MandantId = 1
};
Console.WriteLine($"Sending to API: Date={newBooking.Date.ToString("dd.MM.yyyy HH:mm")} (Kind: {newBooking.Date.Kind})");
try
{
var (success, error) = await BookingService.CreateBookingAsync(newBooking);
if (success)
{
await OnBookingCreated.InvokeAsync();
Console.WriteLine("Booking created successfully.");
}
else
{
await OnError.InvokeAsync(error ?? "Fehler beim Erstellen der Buchung.");
Console.WriteLine($"API error: {error}");
}
}
catch (Exception ex)
{
await OnError.InvokeAsync($"Fehler beim Erstellen der Buchung: {ex.Message}");
Console.WriteLine($"Exception in DropOnSlot: {ex.Message}");
}
draggedOrderId = null;
}
private async Task DeleteBooking(int bookingId)
{
try
{
var (success, error) = await BookingService.DeleteBookingAsync(bookingId);
if (success)
{
await OnBookingCreated.InvokeAsync();
Console.WriteLine("Booking deleted successfully.");
}
else
{
await OnError.InvokeAsync(error ?? "Fehler beim Löschen der Buchung.");
Console.WriteLine($"API error: {error}");
}
}
catch (Exception ex)
{
await OnError.InvokeAsync($"Fehler beim Löschen der Buchung: {ex.Message}");
Console.WriteLine($"Exception in DeleteBooking: {ex.Message}");
}
}
private List<BookingDto> GetBookingsForSlot(DateTime day, int hour)
{
var bookings = Bookings?.Where(b => b.Date.Date == day.Date && b.Date.Hour == hour).ToList() ?? new();
if (bookings.Any())
{
Console.WriteLine($"Bookings for {day.ToString("dd.MM.yyyy")} {hour}:00: {bookings.Count}");
foreach (var booking in bookings)
{
Console.WriteLine($"Booking ID={booking.Id}, Date={booking.Date.ToString("dd.MM.yyyy HH:mm")}, Hours={booking.Hours}");
}
}
return bookings;
}
private bool IsWeekend(DateTime day)
{
return day.DayOfWeek == DayOfWeek.Saturday || day.DayOfWeek == DayOfWeek.Sunday;
}
private decimal GetHoursForDay(DateTime day)
{
var hours = Bookings?.Where(b => b.Date.Date == day.Date).Sum(b => b.Hours) ?? 0;
if (hours > 0)
{
Console.WriteLine($"Hours for {day.ToString("dd.MM.yyyy")}: {hours}");
}
return hours;
}
private string GetHoursColorClass(DateTime day)
{
var hours = GetHoursForDay(day);
if (hours >= 8)
return "text-success";
else if (hours > 6 && hours < 8)
return "text-warning";
else
return "text-danger";
}
}
<style>
.orders-sidebar {
min-width: 200px;
}
.draggable-order {
cursor: grab;
padding: 10px;
margin-bottom: 5px;
}
.week-calendar td {
vertical-align: top;
min-height: 50px;
position: relative;
}
.week-calendar th.weekend {
background-color: #f8f9fa;
}
.week-calendar td.weekend {
background-color: #f8f9fa;
}
.week-calendar td:hover {
background-color: #e9ecef;
}
.week-calendar td.weekend:hover {
background-color: #e2e6ea;
}
.booking-item {
border-radius: 4px;
margin: 2px;
font-size: 0.9em;
overflow: hidden;
}
.text-success {
font-weight: bold;
}
.text-warning {
font-weight: bold;
}
.text-danger {
font-weight: bold;
}
</style>

View File

@@ -69,7 +69,7 @@
private void GoToBookings()
{
Nav.NavigateTo("/bookings");
Nav.NavigateTo("/booking-entry");
}
private void GoToAnalysis()