@inject ITimetrackerService TrackerService @inject AuthenticationStateProvider AuthStateProvider @inject IJSRuntime JS @using System.Security.Claims @using timetracker.Shared @if (_isOpen) {
Timebot
Timebot Online
@foreach (var msg in _messages) {
@if (msg.Sender == "Bot") { Timebot }
@if (msg.IsTyping) {
} else { @((MarkupString)FormatMessageText(msg.Content)) }
}
}
Timebot Launcher @if (_unreadCount > 0 && !_isOpen) { @_unreadCount }
@code { private bool _isOpen = false; private string _userInput = ""; private int _unreadCount = 1; private List _messages = new(); private int _userId; private string _username = "Marc"; private AppSettings _settings = new(); protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); var claim = authState.User.FindFirst(ClaimTypes.NameIdentifier); if (claim != null) { _userId = int.Parse(claim.Value); _username = authState.User.Identity?.Name ?? "Marc"; if (!string.IsNullOrEmpty(_username)) { _username = char.ToUpper(_username[0]) + _username.Substring(1); } _settings = await TrackerService.GetSettingsAsync(_userId); } _messages.Add(new ChatMessage { Sender = "Bot", Content = $"Hallo {_username}! 👋 Ich bin dein Timebot. Ich habe ein Auge auf deine Zeiten geworfen.\n\nWie kann ich dir heute helfen?", Timestamp = DateTime.Now }); } private void ToggleChat() { _isOpen = !_isOpen; if (_isOpen) { _unreadCount = 0; _ = ScrollToBottom(); } } private async Task HandleKeyPress(KeyboardEventArgs e) { if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_userInput)) { await SendMessage(); } } private async Task SendMessage() { if (string.IsNullOrWhiteSpace(_userInput)) return; var text = _userInput; _userInput = ""; _messages.Add(new ChatMessage { Sender = "User", Content = text, Timestamp = DateTime.Now }); await ScrollToBottom(); await RespondTo(text); } private async Task AskQuestion(string key) { string userQuestion = ""; if (key == "Überstunden") userQuestion = "Wie viele Überstunden habe ich?"; else if (key == "Woche") userQuestion = "Wie sieht meine Woche aus?"; else if (key == "Urlaub") userQuestion = "Wie hilft mir der Urlaubs-Maximizer?"; else if (key == "Hilfe") userQuestion = "Welche Fragen kann ich dir stellen?"; _messages.Add(new ChatMessage { Sender = "User", Content = userQuestion, Timestamp = DateTime.Now }); await ScrollToBottom(); await RespondTo(key); } private async Task RespondTo(string query) { // Add typing indicator var typingMsg = new ChatMessage { Sender = "Bot", IsTyping = true, Timestamp = DateTime.Now }; _messages.Add(typingMsg); await ScrollToBottom(); // Simulate thinking/typing delay await Task.Delay(1000); _messages.Remove(typingMsg); string responseText = ""; var normalized = query.ToLower(); if (normalized.Contains("überstunden") || normalized == "überstunden" || normalized.Contains("saldo") || normalized.Contains("gleitzeit")) { var settings = await TrackerService.GetSettingsAsync(_userId); var overtime = await TrackerService.GetTotalOvertimeAsync(_userId, settings); var overtimeStr = FormatTs(overtime, sign: true); if (overtime >= TimeSpan.Zero) { responseText = $"Dein Gleitzeitkonto sieht super aus! Du hast aktuell **{overtimeStr} Std.** auf deinem Gleitzeitkonto angesammelt.\n\nSehr stark! Hast du mit der Extrazeit schon etwas vor oder bist du einfach nur voll am Hustlen? 🚀🔥"; } else { responseText = $"Dein Gleitzeitkonto steht aktuell bei **{overtimeStr} Std.**\n\nDu bist also ein kleines bisschen im Minus. Aber kein Grund zur Sorge: Das lässt sich mit ein paar Extra-Minuten in den nächsten Tagen leicht wieder ausgleichen! 💪"; } } else if (normalized.Contains("woche") || normalized == "woche" || normalized.Contains("arbeitszeit") || normalized.Contains("stunden")) { var today = DateOnly.FromDateTime(DateTime.Today); var monday = GetMonday(today); var settings = await TrackerService.GetSettingsAsync(_userId); var weekDays = await TrackerService.GetWeekAsync(_userId, monday); double workedHours = 0; double targetHours = 0; foreach (var date in Enumerable.Range(0, 7).Select(i => monday.AddDays(i))) { bool isWorkDay = settings.IsWorkDay(date.DayOfWeek); if (isWorkDay) { targetHours += settings.DailyTargetHours; } var wd = weekDays.FirstOrDefault(d => d.Date == date); if (wd != null && wd.StartTime != null && wd.EndTime != null) { var gross = wd.EndTime.Value.ToTimeSpan() - wd.StartTime.Value.ToTimeSpan(); if (gross > TimeSpan.Zero) { 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())); workedHours += (gross - breakTotal).TotalHours; } } } var workedTs = TimeSpan.FromHours(workedHours); var targetTs = TimeSpan.FromHours(targetHours); var percent = targetHours > 0 ? (int)Math.Round(workedHours / targetHours * 100) : 0; responseText = $"Diese Woche hast du bereits **{FormatTs(workedTs)}** von geplanten **{FormatTs(targetTs)}** gearbeitet.\n\nDu hast diese Woche also schon **{percent}%** deines Solls erfüllt. Weiter so! ⏰"; } else if (normalized.Contains("urlaub") || normalized == "urlaub" || normalized.Contains("maximizer") || normalized.Contains("brückentag")) { responseText = $"Der **Urlaubs-Maximizer** analysiert das Kalenderjahr, gesetzliche Feiertage und Brückentage, um dir die besten Zeiträume für Urlaub vorzuschlagen.\n\nDu findest die Auswertung direkt in der Sidebar unter **'Urlaubs-Maximizer'**. So machst du aus wenigen Urlaubstagen maximale Freizeit! 🏖️✈️"; } else if (normalized.Contains("hilfe") || normalized == "hilfe" || normalized.Contains("hallo") || normalized.Contains("hi") || normalized.Contains("helo") || normalized.Contains("huhu") || normalized == "hilfe") { responseText = $"Ich kann dir Auskunft über deine Zeiten geben. Frage mich zum Beispiel:\n\n" + $"• *'Wie viele Überstunden habe ich?'*\n" + $"• *'Wie sieht meine Woche aus?'*\n" + $"• *'Was macht der Urlaubs-Maximizer?'*\n\n" + $"Oder tippe einfach deine Frage ein! 💡"; } else { responseText = $"Entschuldige, diese Frage kann ich leider noch nicht beantworten. 🤖\n\nFrage mich gerne nach deinem **Überstunden-Saldo**, deiner **Wochen-Arbeitszeit** oder dem **Urlaubs-Maximizer**!"; } _messages.Add(new ChatMessage { Sender = "Bot", Content = responseText, Timestamp = DateTime.Now }); await ScrollToBottom(); } private async Task ScrollToBottom() { try { await JS.InvokeVoidAsync("eval", "setTimeout(() => { const el = document.getElementById('timebot-body-el'); if(el) el.scrollTop = el.scrollHeight; }, 50);"); } catch { // Prerender-Pass ignorieren } } private string FormatMessageText(string text) { // Simple helper to replace markdown bold (**text**) with HTML bold (text) // and newlines (\n) with
if (string.IsNullOrEmpty(text)) return ""; var formatted = text; // Escape HTML first to prevent XSS formatted = System.Net.WebUtility.HtmlEncode(formatted); // Replace back escaped ** formatting // Simple regex replace for **text** var regex = new System.Text.RegularExpressions.Regex(@"\*\*(.*?)\*\*"); formatted = regex.Replace(formatted, "$1"); // Replace newlines formatted = formatted.Replace("\n", "
"); return formatted; } private static DateOnly GetMonday(DateOnly date) { int diff = ((int)date.DayOfWeek - (int)DayOfWeek.Monday + 7) % 7; return date.AddDays(-diff); } private static string FormatTs(TimeSpan ts, bool sign = false) { if (ts == TimeSpan.Zero && sign) return "±0:00"; var prefix = sign ? (ts >= TimeSpan.Zero ? "+" : "−") : (ts < TimeSpan.Zero ? "−" : ""); var abs = ts.Duration(); return $"{prefix}{(int)abs.TotalHours}:{abs.Minutes:D2}"; } private sealed class ChatMessage { public string Sender { get; set; } = "Bot"; public string Content { get; set; } = ""; public DateTime Timestamp { get; set; } = DateTime.Now; public bool IsTyping { get; set; } = false; } }