232 lines
7.1 KiB
Text
232 lines
7.1 KiB
Text
|
|
@page "/calendar-event"
|
||
|
|
@page "/calendar-event/{DateStr}"
|
||
|
|
@page "/calendar-event/{DateStr}/{EventId}"
|
||
|
|
@attribute [Authorize]
|
||
|
|
@rendermode InteractiveServer
|
||
|
|
|
||
|
|
@inject IWorkDayService WorkDayService
|
||
|
|
@inject NavigationManager Navigation
|
||
|
|
@inject IJSRuntime JS
|
||
|
|
|
||
|
|
<PageTitle>Calendar Event</PageTitle>
|
||
|
|
|
||
|
|
<h1>Calendar Event</h1>
|
||
|
|
|
||
|
|
@if (!loaded)
|
||
|
|
{
|
||
|
|
<p><em>Loading...</em></p>
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
<div class="row g-3">
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Date</label>
|
||
|
|
<input type="date" class="form-control" value="@selectedDate.ToString("yyyy-MM-dd")" @onchange="OnDateChanged" disabled="@isExistingEvent" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Entry Type</label>
|
||
|
|
<select class="form-select" @bind="eventType">
|
||
|
|
@foreach (var item in Enum.GetValues<CalendarEventType>())
|
||
|
|
{
|
||
|
|
<option value="@item">@item</option>
|
||
|
|
}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-lg-8">
|
||
|
|
<label class="form-label">Description</label>
|
||
|
|
<input class="form-control" @bind="description" maxlength="120" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">Start Time</label>
|
||
|
|
<input type="time" class="form-control" value="@startTimeStr" @onchange="OnStartTimeChanged" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label">End Time</label>
|
||
|
|
<input type="time" class="form-control" value="@endTimeStr" @onchange="OnEndTimeChanged" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="col-12 col-md-6 col-lg-4">
|
||
|
|
<label class="form-label text-muted">Duration</label>
|
||
|
|
<div class="form-control-plaintext fw-bold">@FormatDuration()</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="d-flex align-items-center gap-2 mt-4">
|
||
|
|
<button class="btn btn-primary" @onclick="SaveAsync">Save</button>
|
||
|
|
@if (isExistingEvent)
|
||
|
|
{
|
||
|
|
<button class="btn btn-outline-danger" @onclick="DeleteAsync">Delete</button>
|
||
|
|
}
|
||
|
|
<button class="btn btn-outline-secondary" @onclick="BackToCalendar">Back to Calendar</button>
|
||
|
|
@if (!string.IsNullOrWhiteSpace(statusMessage))
|
||
|
|
{
|
||
|
|
<span class="text-success">@statusMessage</span>
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
@code {
|
||
|
|
[Parameter] public string? DateStr { get; set; }
|
||
|
|
[Parameter] public string? EventId { get; set; }
|
||
|
|
|
||
|
|
private bool loaded;
|
||
|
|
private bool isExistingEvent;
|
||
|
|
private DateOnly selectedDate = DateOnly.FromDateTime(DateTime.Today);
|
||
|
|
private string eventId = string.Empty;
|
||
|
|
private CalendarEventType eventType = CalendarEventType.Generic;
|
||
|
|
private string description = "Calendar entry";
|
||
|
|
private string? startTimeStr;
|
||
|
|
private string? endTimeStr;
|
||
|
|
private string? statusMessage;
|
||
|
|
|
||
|
|
protected override async Task OnInitializedAsync()
|
||
|
|
{
|
||
|
|
if (!string.IsNullOrEmpty(DateStr) && DateOnly.TryParseExact(DateStr, "yyyy-MM-dd", out var parsed))
|
||
|
|
{
|
||
|
|
selectedDate = parsed;
|
||
|
|
}
|
||
|
|
|
||
|
|
await LoadEventAsync();
|
||
|
|
loaded = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task LoadEventAsync()
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(EventId))
|
||
|
|
{
|
||
|
|
SetDefaults();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var existing = await WorkDayService.GetCalendarEventAsync(selectedDate, EventId);
|
||
|
|
if (existing is not null)
|
||
|
|
{
|
||
|
|
eventId = existing.Id;
|
||
|
|
eventType = existing.EventType;
|
||
|
|
description = existing.Description;
|
||
|
|
startTimeStr = existing.StartTime?.ToString("HH:mm");
|
||
|
|
endTimeStr = existing.EndTime?.ToString("HH:mm");
|
||
|
|
isExistingEvent = true;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
SetDefaults();
|
||
|
|
statusMessage = "The selected calendar event was not found. A new event will be created for this day.";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void SetDefaults()
|
||
|
|
{
|
||
|
|
eventId = string.Empty;
|
||
|
|
eventType = CalendarEventType.Generic;
|
||
|
|
description = "Calendar entry";
|
||
|
|
startTimeStr = null;
|
||
|
|
endTimeStr = null;
|
||
|
|
isExistingEvent = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task OnDateChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
if (DateOnly.TryParse(e.Value?.ToString(), out var parsed))
|
||
|
|
{
|
||
|
|
selectedDate = parsed;
|
||
|
|
statusMessage = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Task.CompletedTask;
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task OnStartTimeChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
startTimeStr = e.Value?.ToString();
|
||
|
|
statusMessage = null;
|
||
|
|
return Task.CompletedTask;
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task OnEndTimeChanged(ChangeEventArgs e)
|
||
|
|
{
|
||
|
|
endTimeStr = e.Value?.ToString();
|
||
|
|
statusMessage = null;
|
||
|
|
return Task.CompletedTask;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task SaveAsync()
|
||
|
|
{
|
||
|
|
var calendarEvent = new CalendarEventDocument
|
||
|
|
{
|
||
|
|
Id = eventId,
|
||
|
|
EventType = eventType,
|
||
|
|
Description = description,
|
||
|
|
StartTime = ParseTime(startTimeStr),
|
||
|
|
EndTime = ParseTime(endTimeStr)
|
||
|
|
};
|
||
|
|
|
||
|
|
var saved = await WorkDayService.SaveCalendarEventAsync(selectedDate, calendarEvent);
|
||
|
|
eventId = saved.Id;
|
||
|
|
isExistingEvent = true;
|
||
|
|
startTimeStr = saved.StartTime?.ToString("HH:mm");
|
||
|
|
endTimeStr = saved.EndTime?.ToString("HH:mm");
|
||
|
|
Navigation.NavigateTo($"/calendar/{selectedDate:yyyy-MM}");
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task DeleteAsync()
|
||
|
|
{
|
||
|
|
if (!isExistingEvent || string.IsNullOrWhiteSpace(eventId))
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var confirmed = await JS.InvokeAsync<bool>("confirm", $"Delete calendar event '{description}' on {selectedDate:dddd d MMMM}?\nThis cannot be undone.");
|
||
|
|
if (!confirmed)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var deleted = await WorkDayService.DeleteCalendarEventAsync(selectedDate, eventId);
|
||
|
|
if (deleted)
|
||
|
|
{
|
||
|
|
Navigation.NavigateTo($"/calendar/{selectedDate:yyyy-MM}");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
statusMessage = "Unable to delete the calendar event.";
|
||
|
|
}
|
||
|
|
|
||
|
|
private void BackToCalendar()
|
||
|
|
{
|
||
|
|
Navigation.NavigateTo($"/calendar/{selectedDate:yyyy-MM}");
|
||
|
|
}
|
||
|
|
|
||
|
|
private decimal? GetDuration()
|
||
|
|
{
|
||
|
|
var start = ParseTime(startTimeStr);
|
||
|
|
var end = ParseTime(endTimeStr);
|
||
|
|
if (!start.HasValue || !end.HasValue || end <= start)
|
||
|
|
{
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return Math.Round((decimal)(end.Value - start.Value).TotalHours, 2, MidpointRounding.AwayFromZero);
|
||
|
|
}
|
||
|
|
|
||
|
|
private string FormatDuration() => GetDuration() is { } duration ? FormatDurationHours(duration) : "—";
|
||
|
|
|
||
|
|
private static string FormatDurationHours(decimal value)
|
||
|
|
{
|
||
|
|
var totalMinutes = (int)Math.Round(value * 60m, MidpointRounding.AwayFromZero);
|
||
|
|
var hours = totalMinutes / 60;
|
||
|
|
var minutes = totalMinutes % 60;
|
||
|
|
return $"{hours:00}:{minutes:00}";
|
||
|
|
}
|
||
|
|
|
||
|
|
private static TimeOnly? ParseTime(string? value)
|
||
|
|
{
|
||
|
|
return !string.IsNullOrWhiteSpace(value) && TimeOnly.TryParse(value, out var parsed)
|
||
|
|
? parsed
|
||
|
|
: null;
|
||
|
|
}
|
||
|
|
}
|