@page "/dashboard" @implements IAsyncDisposable @inject IKanbanService KanbanService @inject ICustomerService CustomerService @inject ITireStackService TireStackService @inject IAuthService AuthService @inject UserSessionService UserSession @inject IJSRuntime JS @using Lagerbestand.Components.Shared Dashboard - Reifen Späth Lagerverwaltung @if (!UserSession.IsWorkerOrAdmin) { } else {
@* Error Display *@ @if (!string.IsNullOrEmpty(errorMessage)) { } @* Statistics *@
@statistics.TotalTasks Offene Aufgaben
@statistics.TasksDueToday Heute fällig
@statistics.OverdueTasks Überfällig
@statistics.CompletedThisWeek Diese Woche erledigt
@* Search & Filter Bar *@
@if (showLabelFilter) {
@foreach (var label in GetFilteredLabels()) { }
}
@if (showCustomerFilter) {
@foreach (var customer in GetFilteredFilterCustomers()) { }
}
@if (HasActiveFilters) { }
@if (isLoading) {
Lade Dashboard...
} else if (columns.Count == 0) {

Keine Spalten vorhanden

Das Kanban-Board wurde noch nicht initialisiert.

@if (UserSession.IsAdmin) { }
} else { @* Kanban Board *@
@foreach (var column in columns) { var columnTasks = GetTasksForColumn(column);
@if (!string.IsNullOrEmpty(column.Icon)) { }

@column.Name

@columnTasks.Count
@foreach (var task in columnTasks) {
@RenderTaskCard(task)
}
}
}
@* Create/Edit Task Modal *@ @if (showTaskModal && editingTask != null) { } @* Task Detail Modal *@ @if (showTaskDetailModal && selectedTask != null) { } @* Templates Modal *@ @if (showTemplateModal) { } @* Labels Management Modal (Admin only) *@ @if (showLabelsModal && UserSession.IsAdmin) { } } @code { // === State === private bool isLoading = true; private bool isSaving; private bool isUploading; private string? errorMessage; // === Data === private List columns = new(); private List tasks = new(); private List labels = new(); private List templates = new(); private List customers = new(); private List availableUsers = new(); private KanbanStatistics statistics = new(); // === Drag & Drop === private KanbanTask? draggedTask; private Guid? dragOverColumnId; // === Task Modal === private bool showTaskModal; private KanbanTask? editingTask; private bool isEditingExistingTask; private List selectedLabelIds = new(); private List selectedAssigneeIds = new(); private List selectedCustomerIds = new(); private Dictionary selectedVehicleIds = new(); private List selectedTireStackIds = new(); private List availableTireStacks = new(); private KanbanTaskTemplate? activeTemplate; private List pendingTemplateTodos = new(); private string? templateTitlePattern; private string? templateDescriptionPattern; // === Task Detail Modal === private bool showTaskDetailModal; private KanbanTask? selectedTask; private ElementReference chatContainer; private string newChatMessage = ""; private string newTodoText = ""; private Guid? editingTodoId; private string editingTodoText = ""; private string taskNotesText = ""; private string taskTireSizeNote = ""; // === Templates Modal === private bool showTemplateModal; private bool showTemplateForm; private KanbanTaskTemplate? editingTemplate; private List editingTemplateTodos = new(); private List selectedTemplateLabelIds = new(); private string newTemplateTodoText = ""; // === Labels Modal === private bool showLabelsModal; private string newLabelName = ""; private string newLabelColor = "#3b82f6"; // === Customer Search === private string customerSearchText = ""; // === Board Search & Filter === private string boardSearchText = ""; private Guid? filterLabelId; private Guid? filterCustomerId; private bool showLabelFilter; private bool showCustomerFilter; private string filterLabelSearch = ""; private string filterCustomerSearch = ""; private List recentlyArchivedTasks = new(); private bool confirmDeleteTask; private bool HasActiveFilters => !string.IsNullOrWhiteSpace(boardSearchText) || filterLabelId.HasValue || filterCustomerId.HasValue; protected override async Task OnInitializedAsync() { if (UserSession.IsWorkerOrAdmin) { await LoadDataSafe(); } } private DotNetObjectReference? _visibilityRef; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender && UserSession.IsWorkerOrAdmin) { _visibilityRef = DotNetObjectReference.Create(this); await VisibilityRefresh.RegisterAsync(JS, _visibilityRef, nameof(OnVisibilityReturn), 120); } } [JSInvokable] public async Task OnVisibilityReturn(int hiddenSeconds) { if (!UserSession.IsWorkerOrAdmin) return; await LoadDataSafe(); await InvokeAsync(StateHasChanged); } public async ValueTask DisposeAsync() { await VisibilityRefresh.UnregisterAsync(JS); _visibilityRef?.Dispose(); } private async Task LoadDataSafe() { isLoading = true; errorMessage = null; StateHasChanged(); try { columns = await KanbanService.GetColumnsAsync(); tasks = await KanbanService.GetTasksAsync(); labels = await KanbanService.GetLabelsAsync(); templates = await KanbanService.GetTemplatesAsync(); customers = await CustomerService.GetAllAsync(); availableUsers = await AuthService.GetAllUsersAsync(); statistics = await KanbanService.GetStatisticsAsync(); recentlyArchivedTasks = await KanbanService.GetRecentlyArchivedTasksAsync(7); // Auto-archive old completed tasks (run sequentially to avoid DbContext concurrency) try { await KanbanService.ArchiveOldCompletedTasksAsync(30); } catch { // Logged in service, non-critical } } catch (Exception ex) { errorMessage = $"Fehler beim Laden: {ex.Message}"; } finally { isLoading = false; StateHasChanged(); } } private async Task RefreshData() { try { tasks = await KanbanService.GetTasksAsync(); recentlyArchivedTasks = await KanbanService.GetRecentlyArchivedTasksAsync(7); statistics = await KanbanService.GetStatisticsAsync(); StateHasChanged(); } catch (Exception ex) { errorMessage = $"Fehler beim Aktualisieren: {ex.Message}"; } } private List GetTasksForColumn(KanbanColumn column) { var columnTasks = tasks.Where(t => t.ColumnId == column.Id).AsEnumerable(); // Text search if (!string.IsNullOrWhiteSpace(boardSearchText)) { var search = boardSearchText.Trim(); columnTasks = columnTasks.Where(t => (t.Title?.Contains(search, StringComparison.OrdinalIgnoreCase) == true) || (t.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) == true)); } // Label filter if (filterLabelId.HasValue) { columnTasks = columnTasks.Where(t => t.TaskLabels?.Any(tl => tl.LabelId == filterLabelId.Value) == true); } // Customer filter if (filterCustomerId.HasValue) { columnTasks = columnTasks.Where(t => t.TaskCustomers?.Any(tc => tc.CustomerId == filterCustomerId.Value) == true); } if (column.IsCompletedColumn) { // Include recently archived tasks (≤7 days) in the completed column for easy restore var archivedForColumn = recentlyArchivedTasks.AsEnumerable(); // Apply same filters to archived tasks if (!string.IsNullOrWhiteSpace(boardSearchText)) { var search = boardSearchText.Trim(); archivedForColumn = archivedForColumn.Where(t => (t.Title?.Contains(search, StringComparison.OrdinalIgnoreCase) == true) || (t.Description?.Contains(search, StringComparison.OrdinalIgnoreCase) == true)); } if (filterLabelId.HasValue) { archivedForColumn = archivedForColumn.Where(t => t.TaskLabels?.Any(tl => tl.LabelId == filterLabelId.Value) == true); } if (filterCustomerId.HasValue) { archivedForColumn = archivedForColumn.Where(t => t.TaskCustomers?.Any(tc => tc.CustomerId == filterCustomerId.Value) == true); } return columnTasks .Concat(archivedForColumn) .OrderByDescending(t => t.CompletedAt ?? t.UpdatedAt) .ToList(); } return columnTasks.OrderBy(t => t.SortOrder).ToList(); } private string GetColumnHeaderStyle(KanbanColumn column) { if (!string.IsNullOrEmpty(column.Color)) { return $"border-top-color: {column.Color}"; } return ""; } private async Task InitializeDefaultColumns() { try { // Manually create default columns if they don't exist var defaultColumns = new[] { ("📥", "Backlog", "#6b7280", true, false), ("📋", "ToDo", "#3b82f6", false, false), ("🔄", "In Arbeit", "#f59e0b", false, false), ("⏳", "Warte auf Kunden", "#8b5cf6", false, false), ("✅", "Erledigt", "#10b981", false, true) }; foreach (var (icon, name, color, isDefault, isCompleted) in defaultColumns) { await KanbanService.CreateColumnAsync(new KanbanColumn { Name = name, Icon = icon, Color = color, IsDefault = isDefault, IsCompletedColumn = isCompleted }); } await LoadDataSafe(); } catch (Exception ex) { errorMessage = $"Fehler beim Erstellen der Spalten: {ex.Message}"; } } #region Task Card Rendering private RenderFragment RenderTaskCard(KanbanTask task) => __builder => {
@if (task.IsArchived) { 📦 } @task.Title
@if (task.TaskLabels?.Any() == true) {
@foreach (var tl in task.TaskLabels.Take(3)) { @if (tl.Label != null) { } } @if (task.TaskLabels.Count > 3) { +@(task.TaskLabels.Count - 3) }
}
}; private string GetTaskCardClasses(KanbanTask task) { var classes = new List(); if (task.IsArchived) classes.Add("archived"); else if (task.IsOverdue) classes.Add("overdue"); else if (task.IsDueToday) classes.Add("due-today"); classes.Add($"priority-{task.Priority.ToString().ToLower()}"); if (draggedTask?.Id == task.Id) classes.Add("dragging"); return string.Join(" ", classes); } private string FormatEstimatedTime(int? minutes) { if (!minutes.HasValue || minutes.Value <= 0) return ""; if (minutes.Value < 60) return $"{minutes.Value}m"; var hours = minutes.Value / 60; var mins = minutes.Value % 60; return mins > 0 ? $"{hours}h {mins}m" : $"{hours}h"; } #endregion #region Drag & Drop private void HandleDragStart(KanbanTask task) { draggedTask = task; } private void HandleDragEnd() { draggedTask = null; dragOverColumnId = null; StateHasChanged(); } private void HandleDragOver() { // Required for drop to work } private void HandleDragEnter(Guid columnId) { if (draggedTask != null) { dragOverColumnId = columnId; } } private void HandleDragLeave() { // Don't clear immediately to avoid flickering } private async Task HandleDrop(Guid columnId) { if (draggedTask != null && draggedTask.ColumnId != columnId) { try { await KanbanService.MoveTaskAsync(draggedTask.Id, columnId, 0, UserSession.UserId); await RefreshData(); } catch (Exception ex) { errorMessage = $"Fehler beim Verschieben: {ex.Message}"; } } draggedTask = null; dragOverColumnId = null; } private async Task OnTaskColumnChange(Guid taskId, ChangeEventArgs e) { if (Guid.TryParse(e.Value?.ToString(), out var newColumnId)) { try { await KanbanService.MoveTaskAsync(taskId, newColumnId, 0, UserSession.UserId); await RefreshData(); } catch (Exception ex) { errorMessage = $"Fehler beim Verschieben: {ex.Message}"; } } } #endregion #region Task Modal private void OpenCreateTaskModal(Guid? columnId) { var defaultColumnId = columnId ?? columns.FirstOrDefault(c => c.IsDefault)?.Id ?? columns.FirstOrDefault()?.Id ?? Guid.Empty; editingTask = new KanbanTask { ColumnId = defaultColumnId, Priority = KanbanTaskPriority.Medium }; isEditingExistingTask = false; selectedLabelIds.Clear(); selectedAssigneeIds.Clear(); selectedCustomerIds.Clear(); selectedVehicleIds.Clear(); selectedTireStackIds.Clear(); availableTireStacks.Clear(); customerSearchText = ""; showTaskModal = true; } private void CloseTaskModal() { showTaskModal = false; editingTask = null; isEditingExistingTask = false; activeTemplate = null; pendingTemplateTodos.Clear(); templateTitlePattern = null; templateDescriptionPattern = null; customerSearchText = ""; selectedVehicleIds.Clear(); } private async Task EditSelectedTask() { if (selectedTask == null) return; // Copy task data for editing editingTask = new KanbanTask { Id = selectedTask.Id, Title = selectedTask.Title, Description = selectedTask.Description, ColumnId = selectedTask.ColumnId, Priority = selectedTask.Priority, DueDate = selectedTask.DueDate, EstimatedMinutes = selectedTask.EstimatedMinutes, SortOrder = selectedTask.SortOrder, NotesText = selectedTask.NotesText, TireSizeNote = selectedTask.TireSizeNote }; // Populate selected labels selectedLabelIds = selectedTask.TaskLabels?.Select(tl => tl.LabelId).ToList() ?? new List(); // Populate selected assignees selectedAssigneeIds = selectedTask.Assignees?.Select(a => a.UserId).ToList() ?? new List(); // Populate selected customers selectedCustomerIds = selectedTask.TaskCustomers?.Select(tc => tc.CustomerId).ToList() ?? new List(); // Clear vehicle selections (user re-picks vehicles when editing) selectedVehicleIds.Clear(); // Load tire stacks for selected customers availableTireStacks.Clear(); await LoadTireStacksForCustomers(); // Populate selected tire stacks selectedTireStackIds = selectedTask.TaskTireStacks?.Select(ts => ts.TireStackId).ToList() ?? new List(); isEditingExistingTask = true; showTaskDetailModal = false; showTaskModal = true; } private async Task SaveTask() { if (editingTask == null || string.IsNullOrWhiteSpace(editingTask.Title)) { errorMessage = "Bitte geben Sie einen Titel ein."; return; } isSaving = true; StateHasChanged(); try { KanbanTask savedTask; if (isEditingExistingTask && editingTask.Id != Guid.Empty) { savedTask = await KanbanService.UpdateTaskAsync(editingTask, UserSession.UserId); } else { savedTask = await KanbanService.CreateTaskAsync(editingTask, UserSession.UserId); } // Update relations await KanbanService.SetTaskLabelsAsync(savedTask.Id, selectedLabelIds); await KanbanService.SetAssigneesAsync(savedTask.Id, selectedAssigneeIds, UserSession.UserId); var customerVehicleMap = selectedCustomerIds.ToDictionary( cid => cid, cid => selectedVehicleIds.TryGetValue(cid, out var vid) ? vid : null); await KanbanService.SetTaskCustomersAsync(savedTask.Id, customerVehicleMap); await KanbanService.SetTaskTireStacksAsync(savedTask.Id, selectedTireStackIds); // Add template todos if creating from template if (!isEditingExistingTask && pendingTemplateTodos.Count > 0) { foreach (var todoText in pendingTemplateTodos) { await KanbanService.AddTodoAsync(savedTask.Id, todoText); } } CloseTaskModal(); await RefreshData(); } catch (Exception ex) { errorMessage = $"Fehler beim Speichern: {ex.Message}"; } finally { isSaving = false; StateHasChanged(); } } private void ToggleLabel(Guid labelId, bool selected) { if (selected && !selectedLabelIds.Contains(labelId)) selectedLabelIds.Add(labelId); else if (!selected) selectedLabelIds.Remove(labelId); } private void ToggleAssignee(Guid userId, bool selected) { if (selected && !selectedAssigneeIds.Contains(userId)) selectedAssigneeIds.Add(userId); else if (!selected) selectedAssigneeIds.Remove(userId); } private async Task OnCustomerSelected(ChangeEventArgs e) { if (Guid.TryParse(e.Value?.ToString(), out var customerId) && !selectedCustomerIds.Contains(customerId)) { await SelectCustomerById(customerId); } } private List GetFilteredCustomers() { var available = customers.Where(c => !selectedCustomerIds.Contains(c.Id)); if (!string.IsNullOrWhiteSpace(customerSearchText)) { var search = customerSearchText.Trim().ToLower(); available = available.Where(c => (c.FullName?.ToLower().Contains(search) == true) || (c.CustomerNumber?.ToLower().Contains(search) == true) || (c.Company?.ToLower().Contains(search) == true) || (c.Email?.ToLower().Contains(search) == true) || (c.Phone?.ToLower().Contains(search) == true)); } return available.Take(10).ToList(); } private async Task SelectCustomerFromSearch(Guid customerId) { customerSearchText = ""; await SelectCustomerById(customerId); } private async Task SelectCustomerById(Guid customerId) { if (!selectedCustomerIds.Contains(customerId)) { selectedCustomerIds.Add(customerId); await LoadTireStacksForCustomers(); // Auto-select vehicle if customer has exactly 1 active vehicle var customer = customers.FirstOrDefault(c => c.Id == customerId); if (customer != null) { var activeVehicles = customer.Vehicles?.Where(v => v.IsActive).ToList() ?? new(); if (activeVehicles.Count == 1) { selectedVehicleIds[customerId] = activeVehicles[0].Id; AutoFillTireSizeFromVehicle(activeVehicles[0]); } } // Resolve template placeholders when first customer is selected if (activeTemplate != null && editingTask != null && selectedCustomerIds.Count == 1) { if (customer != null) { var selectedVehicle = selectedVehicleIds.TryGetValue(customerId, out var vid) && vid.HasValue ? customer.Vehicles?.FirstOrDefault(v => v.Id == vid.Value) : null; editingTask.Title = ResolveClientPlaceholders(templateTitlePattern, customer, selectedVehicle); editingTask.Description = ResolveClientPlaceholders(templateDescriptionPattern, customer, selectedVehicle); pendingTemplateTodos = pendingTemplateTodos .Select(text => ResolveClientPlaceholders(text, customer, selectedVehicle)) .ToList(); } } } } private async Task RemoveCustomer(Guid customerId) { selectedCustomerIds.Remove(customerId); selectedVehicleIds.Remove(customerId); selectedTireStackIds.RemoveAll(id => availableTireStacks.Any(ts => ts.Id == id && ts.CustomerId == customerId)); await LoadTireStacksForCustomers(); } private void OnVehicleSelected(Guid customerId, Guid? vehicleId) { if (vehicleId.HasValue && vehicleId.Value != Guid.Empty) { selectedVehicleIds[customerId] = vehicleId.Value; var customer = customers.FirstOrDefault(c => c.Id == customerId); var vehicle = customer?.Vehicles?.FirstOrDefault(v => v.Id == vehicleId.Value); if (vehicle != null) { AutoFillTireSizeFromVehicle(vehicle); } } else { selectedVehicleIds.Remove(customerId); } } private void AutoFillTireSizeFromVehicle(Vehicle vehicle) { if (editingTask != null && string.IsNullOrWhiteSpace(editingTask.TireSizeNote) && !string.IsNullOrWhiteSpace(vehicle.TireSize)) { editingTask.TireSizeNote = vehicle.TireSize; } } private async Task LoadTireStacksForCustomers() { availableTireStacks.Clear(); foreach (var customerId in selectedCustomerIds) { try { var stacks = await TireStackService.GetByCustomerAsync(customerId); availableTireStacks.AddRange(stacks); } catch { /* ignore errors loading tire stacks */ } } StateHasChanged(); } private void ToggleTireStack(Guid tireStackId, bool selected) { if (selected && !selectedTireStackIds.Contains(tireStackId)) selectedTireStackIds.Add(tireStackId); else if (!selected) selectedTireStackIds.Remove(tireStackId); } #endregion #region Task Detail Modal private async Task ShowTaskDetail(KanbanTask task) { try { selectedTask = await KanbanService.GetTaskByIdAsync(task.Id); if (selectedTask != null) { taskNotesText = selectedTask.NotesText ?? ""; taskTireSizeNote = selectedTask.TireSizeNote ?? ""; showTaskDetailModal = true; await ScrollChatToBottom(); } } catch (Exception ex) { errorMessage = $"Fehler beim Laden der Aufgabe: {ex.Message}"; } } private void CloseTaskDetailModal() { showTaskDetailModal = false; selectedTask = null; confirmDeleteTask = false; ResetDetailInputs(); } private void ResetDetailInputs() { newChatMessage = ""; newTodoText = ""; editingTodoId = null; editingTodoText = ""; taskNotesText = ""; taskTireSizeNote = ""; } private async Task ScrollChatToBottom() { await Task.Delay(100); try { await JS.InvokeVoidAsync("eval", "document.querySelector('.chat-messages')?.scrollTo(0, document.querySelector('.chat-messages')?.scrollHeight || 0)"); } catch { /* ignore scroll errors */ } } private async Task ReloadSelectedTask() { if (selectedTask == null) return; try { selectedTask = await KanbanService.GetTaskByIdAsync(selectedTask.Id); StateHasChanged(); } catch { /* ignore reload errors */ } } // Todos private async Task ToggleTodoItem(Guid todoId) { try { await KanbanService.ToggleTodoAsync(todoId); await ReloadSelectedTask(); await RefreshData(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } private async Task AddTodo() { if (string.IsNullOrWhiteSpace(newTodoText) || selectedTask == null) return; try { await KanbanService.AddTodoAsync(selectedTask.Id, newTodoText.Trim()); newTodoText = ""; await ReloadSelectedTask(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } private void StartEditTodo(KanbanTaskTodo todo) { editingTodoId = todo.Id; editingTodoText = todo.Text; } private async Task SaveTodoEdit() { if (editingTodoId == null || string.IsNullOrWhiteSpace(editingTodoText)) { editingTodoId = null; return; } try { var todo = selectedTask?.Todos?.FirstOrDefault(t => t.Id == editingTodoId); if (todo != null) { todo.Text = editingTodoText.Trim(); await KanbanService.UpdateTodoAsync(todo); await ReloadSelectedTask(); } } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } finally { editingTodoId = null; editingTodoText = ""; } } private async Task HandleTodoKeyDown(KeyboardEventArgs e) { if (e.Key == "Enter") await SaveTodoEdit(); else if (e.Key == "Escape") { editingTodoId = null; editingTodoText = ""; } } private async Task HandleNewTodoKeyDown(KeyboardEventArgs e) { if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(newTodoText)) { await AddTodo(); } } private async Task DeleteTodo(Guid todoId) { try { await KanbanService.DeleteTodoAsync(todoId); await ReloadSelectedTask(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } // Chat private async Task SendChatMessage() { if (string.IsNullOrWhiteSpace(newChatMessage) || selectedTask == null) return; try { await KanbanService.AddCommentAsync(selectedTask.Id, newChatMessage.Trim(), UserSession.UserId); newChatMessage = ""; await ReloadSelectedTask(); await ScrollChatToBottom(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } // Notes - Simple text field private async Task SaveTaskNotes() { if (selectedTask == null) return; try { selectedTask.NotesText = taskNotesText; await KanbanService.UpdateTaskAsync(selectedTask, UserSession.UserId); } catch (Exception ex) { errorMessage = $"Fehler beim Speichern der Notizen: {ex.Message}"; } } // Tire Size Note - inline editable private async Task SaveTaskTireSizeNote() { if (selectedTask == null) return; try { selectedTask.TireSizeNote = string.IsNullOrWhiteSpace(taskTireSizeNote) ? null : taskTireSizeNote.Trim(); await KanbanService.UpdateTaskAsync(selectedTask, UserSession.UserId); } catch (Exception ex) { errorMessage = $"Fehler beim Speichern der Reifengröße: {ex.Message}"; } } // Attachments private async Task HandleFileUpload(InputFileChangeEventArgs e) { if (selectedTask == null) return; isUploading = true; StateHasChanged(); try { foreach (var file in e.GetMultipleFiles(10)) { if (file.Size > 10 * 1024 * 1024) { errorMessage = $"Datei '{file.Name}' ist zu groß (max. 10 MB)."; continue; } using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024); using var ms = new MemoryStream(); await stream.CopyToAsync(ms); await KanbanService.AddAttachmentAsync(selectedTask.Id, file.Name, file.ContentType, ms.ToArray(), UserSession.UserId); } await ReloadSelectedTask(); } catch (Exception ex) { errorMessage = $"Fehler beim Hochladen: {ex.Message}"; } finally { isUploading = false; StateHasChanged(); } } private async Task DeleteAttachment(Guid attachmentId) { try { await KanbanService.DeleteAttachmentAsync(attachmentId); await ReloadSelectedTask(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } // Archive private async Task ArchiveTask() { if (selectedTask == null) return; try { await KanbanService.ArchiveTaskAsync(selectedTask.Id); CloseTaskDetailModal(); await RefreshData(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } // Unarchive (restore) private async Task UnarchiveTask() { if (selectedTask == null) return; try { await KanbanService.UnarchiveTaskAsync(selectedTask.Id); CloseTaskDetailModal(); await RefreshData(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } // Delete archived task permanently private async Task DeleteArchivedTask() { if (selectedTask == null || !selectedTask.IsArchived) return; try { await KanbanService.DeleteTaskAsync(selectedTask.Id); CloseTaskDetailModal(); await RefreshData(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } private void CancelDeleteTask() { confirmDeleteTask = false; } #endregion #region Board Search & Filter private void ToggleLabelFilter() { showLabelFilter = !showLabelFilter; showCustomerFilter = false; filterLabelSearch = ""; filterCustomerSearch = ""; } private void ToggleCustomerFilter() { showCustomerFilter = !showCustomerFilter; showLabelFilter = false; filterCustomerSearch = ""; filterLabelSearch = ""; } private void SelectLabelFilter(Guid? labelId) { filterLabelId = labelId; showLabelFilter = false; filterLabelSearch = ""; } private void SelectCustomerFilter(Guid? customerId) { filterCustomerId = customerId; showCustomerFilter = false; filterCustomerSearch = ""; } private void ClearAllFilters() { boardSearchText = ""; filterLabelId = null; filterCustomerId = null; showLabelFilter = false; showCustomerFilter = false; filterLabelSearch = ""; filterCustomerSearch = ""; } private IEnumerable GetFilteredLabels() { if (string.IsNullOrWhiteSpace(filterLabelSearch)) return labels; var search = filterLabelSearch.Trim(); return labels.Where(l => l.Name.Contains(search, StringComparison.OrdinalIgnoreCase)); } private IEnumerable GetFilteredFilterCustomers() { var sorted = customers.OrderBy(c => c.LastName).ThenBy(c => c.FirstName).AsEnumerable(); if (string.IsNullOrWhiteSpace(filterCustomerSearch)) return sorted; var search = filterCustomerSearch.Trim(); return sorted.Where(c => (c.FullName?.Contains(search, StringComparison.OrdinalIgnoreCase) == true) || (c.CustomerNumber?.Contains(search, StringComparison.OrdinalIgnoreCase) == true) || (c.Company?.Contains(search, StringComparison.OrdinalIgnoreCase) == true)); } #endregion #region Templates private void ShowTemplateModal() { showTemplateModal = true; showTemplateForm = false; editingTemplate = null; } private void CloseTemplateModal() { showTemplateModal = false; showTemplateForm = false; editingTemplate = null; editingTemplateTodos.Clear(); selectedTemplateLabelIds.Clear(); newTemplateTodoText = ""; } private void ShowCreateTemplateForm() { editingTemplate = new KanbanTaskTemplate { Id = Guid.Empty, DefaultPriority = KanbanTaskPriority.Medium }; editingTemplateTodos = new List(); selectedTemplateLabelIds = new List(); newTemplateTodoText = ""; showTemplateForm = true; } private void EditTemplate(KanbanTaskTemplate template) { editingTemplate = new KanbanTaskTemplate { Id = template.Id, Name = template.Name, Description = template.Description, TitlePattern = template.TitlePattern, DefaultDescription = template.DefaultDescription, DefaultColumnId = template.DefaultColumnId, DefaultPriority = template.DefaultPriority, DefaultEstimatedMinutes = template.DefaultEstimatedMinutes }; editingTemplateTodos = template.Todos? .OrderBy(t => t.SortOrder) .Select(t => new KanbanTaskTemplateTodo { Id = t.Id, TemplateId = t.TemplateId, Text = t.Text, SortOrder = t.SortOrder }) .ToList() ?? new List(); selectedTemplateLabelIds = template.TemplateLabels? .Select(tl => tl.LabelId) .ToList() ?? new List(); newTemplateTodoText = ""; showTemplateForm = true; } private void CancelTemplateForm() { showTemplateForm = false; editingTemplate = null; editingTemplateTodos.Clear(); selectedTemplateLabelIds.Clear(); newTemplateTodoText = ""; } private async Task SaveTemplate() { if (editingTemplate == null || string.IsNullOrWhiteSpace(editingTemplate.Name)) return; try { if (editingTemplate.Id == Guid.Empty) { var created = await KanbanService.CreateTemplateAsync(editingTemplate); if (editingTemplateTodos.Count > 0) { await KanbanService.SyncTemplateTodosAsync(created.Id, editingTemplateTodos); } await KanbanService.SetTemplateLabelsAsync(created.Id, selectedTemplateLabelIds); } else { await KanbanService.UpdateTemplateAsync(editingTemplate); await KanbanService.SyncTemplateTodosAsync(editingTemplate.Id, editingTemplateTodos); await KanbanService.SetTemplateLabelsAsync(editingTemplate.Id, selectedTemplateLabelIds); } templates = await KanbanService.GetTemplatesAsync(); CancelTemplateForm(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } private async Task DeleteTemplate(Guid templateId) { try { await KanbanService.DeleteTemplateAsync(templateId); templates = await KanbanService.GetTemplatesAsync(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } private void OpenCreateFromTemplateModal(KanbanTaskTemplate template) { CloseTemplateModal(); var defaultColumnId = template.DefaultColumnId ?? columns.FirstOrDefault(c => c.IsDefault)?.Id ?? columns.FirstOrDefault()?.Id ?? Guid.Empty; editingTask = new KanbanTask { Title = template.TitlePattern ?? string.Empty, Description = template.DefaultDescription, ColumnId = defaultColumnId, Priority = template.DefaultPriority, EstimatedMinutes = template.DefaultEstimatedMinutes }; isEditingExistingTask = false; selectedLabelIds = template.TemplateLabels?.Select(tl => tl.LabelId).ToList() ?? new(); selectedAssigneeIds.Clear(); selectedCustomerIds.Clear(); selectedVehicleIds.Clear(); selectedTireStackIds.Clear(); availableTireStacks.Clear(); activeTemplate = template; templateTitlePattern = template.TitlePattern; templateDescriptionPattern = template.DefaultDescription; pendingTemplateTodos = template.Todos? .OrderBy(t => t.SortOrder) .Select(t => t.Text) .ToList() ?? new(); showTaskModal = true; } private static string ResolveClientPlaceholders(string? text, Customer customer, Vehicle? selectedVehicle = null) { if (string.IsNullOrEmpty(text)) return text ?? string.Empty; var vehicle = selectedVehicle ?? customer.Vehicles? .Where(v => v.IsActive) .OrderByDescending(v => v.IsPrimary) .ThenBy(v => v.CreatedAt) .FirstOrDefault(); var result = text; result = result.Replace("{Kunde}", customer.FullName); result = result.Replace("{Vorname}", customer.FirstName ?? ""); result = result.Replace("{Nachname}", customer.LastName); result = result.Replace("{Firma}", customer.Company ?? ""); result = result.Replace("{Kundennummer}", customer.CustomerNumber); result = result.Replace("{Telefon}", customer.Phone ?? ""); result = result.Replace("{Email}", customer.Email ?? ""); result = result.Replace("{Fahrzeug}", vehicle?.DisplayName ?? ""); result = result.Replace("{Reifengröße}", vehicle?.TireSize ?? ""); result = result.Replace("{Marke}", vehicle?.Make ?? ""); result = result.Replace("{Modell}", vehicle?.Model ?? ""); result = result.Replace("{Datum}", DateTime.Now.ToString("dd.MM.yyyy")); result = result.Replace("{Jahr}", DateTime.Now.Year.ToString()); return result; } private void AddTemplateTodoToForm() { if (string.IsNullOrWhiteSpace(newTemplateTodoText)) return; editingTemplateTodos.Add(new KanbanTaskTemplateTodo { Id = Guid.NewGuid(), Text = newTemplateTodoText.Trim(), SortOrder = editingTemplateTodos.Count }); newTemplateTodoText = ""; } private void RemoveTemplateTodoFromForm(KanbanTaskTemplateTodo todo) { editingTemplateTodos.Remove(todo); } private void MoveTemplateTodoUp(int index) { if (index <= 0) return; (editingTemplateTodos[index], editingTemplateTodos[index - 1]) = (editingTemplateTodos[index - 1], editingTemplateTodos[index]); } private void MoveTemplateTodoDown(int index) { if (index >= editingTemplateTodos.Count - 1) return; (editingTemplateTodos[index], editingTemplateTodos[index + 1]) = (editingTemplateTodos[index + 1], editingTemplateTodos[index]); } private void HandleTemplateTodoKeyDown(KeyboardEventArgs e) { if (e.Key == "Enter") { AddTemplateTodoToForm(); } } private void ToggleTemplateLabel(Guid labelId, bool selected) { if (selected && !selectedTemplateLabelIds.Contains(labelId)) selectedTemplateLabelIds.Add(labelId); else if (!selected) selectedTemplateLabelIds.Remove(labelId); } #endregion #region Labels private void ShowLabelsModal() { showLabelsModal = true; newLabelName = ""; newLabelColor = "#3b82f6"; } private void CloseLabelsModal() { showLabelsModal = false; newLabelName = ""; newLabelColor = "#3b82f6"; } private async Task AddLabel() { if (string.IsNullOrWhiteSpace(newLabelName)) return; try { await KanbanService.CreateLabelAsync(new KanbanLabel { Name = newLabelName.Trim(), Color = newLabelColor }); newLabelName = ""; newLabelColor = "#3b82f6"; labels = await KanbanService.GetLabelsAsync(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } private async Task DeleteLabel(Guid labelId) { try { await KanbanService.DeleteLabelAsync(labelId); labels = await KanbanService.GetLabelsAsync(); } catch (Exception ex) { errorMessage = $"Fehler: {ex.Message}"; } } #endregion #region Helpers private string GetSeasonIcon(TireSeason season) => season switch { TireSeason.Sommer => "☀️", TireSeason.Winter => "❄️", TireSeason.Allrad => "🔄", _ => "🔄" }; private string GetPriorityIcon(KanbanTaskPriority priority) => priority switch { KanbanTaskPriority.Low => "⬇️", KanbanTaskPriority.Medium => "➡️", KanbanTaskPriority.High => "⬆️", KanbanTaskPriority.Urgent => "🔥", _ => "➡️" }; private string GetPriorityText(KanbanTaskPriority priority) => priority switch { KanbanTaskPriority.Low => "Niedrig", KanbanTaskPriority.Medium => "Mittel", KanbanTaskPriority.High => "Hoch", KanbanTaskPriority.Urgent => "Dringend", _ => "Mittel" }; private char GetUserInitial(AppUser? user) { if (user == null) return '?'; var name = user.DisplayName ?? user.Username ?? "?"; return name.Length > 0 ? char.ToUpper(name[0]) : '?'; } #endregion }