OnProfNext/OnProfNext.Client/Pages/Projects/ProjectDetails.razor
2025-10-15 13:29:50 +02:00

426 lines
16 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@page "/projects/{id:int}"
@using OnProfNext.Client.Services
@inject ProjectApiService ProjectService
@inject NavigationManager Nav
@inject OrderApiService OrderService
@inject UserApiService UserService
@using OnProfNext.Shared.Models.DTOs
<h3 class="mb-4 d-flex justify-content-between align-items-center">
<span class="text-primary">📁 Projekt-Details</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">Projekt wird geladen...</p>
</div>
}
else if (errorMessage is not null)
{
<div class="alert alert-danger">@errorMessage</div>
}
else if (project is null)
{
<div class="alert alert-warning">Projekt nicht gefunden.</div>
}
else
{
<!-- HEADER -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start">
<div>
<h3 class="text-primary mb-1">@project.ProjectName</h3>
<span class="badge bg-@GetStatusColor(project.Status)">
@project.Status
</span>
<p class="mt-3 text-muted">@project.Description</p>
</div>
<div class="text-end">
<small class="text-muted d-block">
<i class="bi bi-calendar"></i>
@project.StartDate.ToString("dd.MM.yyyy")
@if (project.EndDate is not null)
{
<span> @project.EndDate?.ToString("dd.MM.yyyy")</span>
}
</small>
<small class="text-muted d-block mt-1">
<i class="bi bi-building"></i> Mandant: @project.MandantId
</small>
</div>
</div>
@if (project.ProjectManagers?.Any() == true)
{
<div class="mt-3">
<h6 class="fw-bold text-secondary">Projektleiter</h6>
@foreach (var manager in project.ProjectManagers)
{
<span class="badge bg-primary me-1">@manager.FullName</span>
}
</div>
}
</div>
</div>
<!-- TABS -->
<ul class="nav nav-tabs mb-3">
@foreach (var tab in tabs)
{
<li class="nav-item">
<button class="nav-link @(activeTab == tab.Key ? "active" : "")"
@onclick="() => SetActiveTab(tab.Key)">
<i class="@tab.Icon me-1"></i> @tab.Title
</button>
</li>
}
</ul>
<div class="tab-content p-3 border rounded bg-white shadow-sm">
@switch (activeTab)
{
case "overview":
<div>
<h5>Projektübersicht</h5>
<p>Hier kommt später ein Überblick über Fortschritt, KPIs und Aktivitäten.</p>
</div>
break;
case "tasks":
<div>
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="m-0">Aufträge</h5>
<button class="btn btn-success btn-sm" @onclick="ShowAddOrderModal">
<i class="bi bi-plus-circle"></i> Neuer Auftrag
</button>
</div>
@if (orders is null)
{
<div class="text-center text-muted mt-3">
<div class="spinner-border spinner-border-sm text-primary"></div>
<p>Aufträge werden geladen...</p>
</div>
}
else if (!orders.Any())
{
<div class="alert alert-info text-center">Keine Aufträge vorhanden.</div>
}
else
{
<table class="table table-hover align-middle">
<thead class="table-light">
<tr>
<th>Nr.</th>
<th>Titel</th>
<th>Status</th>
<th>Plan (h)</th>
<th>Ist (h)</th>
<th>Mitarbeiter</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var o in orders)
{
<tr>
<td>@o.Auftragsnummer</td>
<td>@o.Titel</td>
<td>
<span class="badge bg-@GetStatusColor(o.Status)">@o.Status</span>
</td>
<td>@o.Planstunden</td>
<td>@o.Iststunden</td>
<td>
<!-- Mitarbeiter-Zuordnung (später) -->
<span class="text-muted small">noch offen</span>
</td>
<td class="text-end">
<button class="btn btn-outline-danger btn-sm" @onclick="() => DeleteOrder(o.Id)">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>
}
</tbody>
</table>
}
</div>
@if (showAddOrderModal)
{
<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">Neuen Auftrag anlegen</h5>
<button type="button" class="btn-close btn-close-white" @onclick="CloseAddOrderModal"></button>
</div>
<div class="modal-body">
<EditForm Model="newOrder" OnValidSubmit="CreateOrderAsync">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label class="form-label">Auftragsnummer</label>
<InputText class="form-control" @bind-Value="newOrder.Auftragsnummer" />
</div>
<div class="mb-3">
<label class="form-label">Titel</label>
<InputText class="form-control" @bind-Value="newOrder.Titel" />
</div>
<div class="mb-3">
<label class="form-label">Planstunden</label>
<InputNumber class="form-control" @bind-Value="newOrder.Planstunden" />
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<InputSelect class="form-select" @bind-Value="newOrder.Status">
<option value="Geplant">Geplant</option>
<option value="In Arbeit">In Arbeit</option>
<option value="Abgeschlossen">Abgeschlossen</option>
</InputSelect>
</div>
<div class="mb-3">
<label class="form-label">Mitarbeiter zuweisen</label>
<InputText class="form-control mb-2" @bind-Value="searchText" placeholder="Mitarbeiter suchen..." />
<div class="list-group" style="max-height: 150px; overflow-y: auto;">
@foreach (var u in FilteredUsers)
{
<label class="list-group-item small">
<input type="checkbox"
value="@u.Id"
checked="@(selectedUserIds.Contains(u.Id))"
@onchange="(e => ToggleUser(u.Id, (bool)e.Value!))" />
@u.FirstName @u.LastName (@u.Username)
</label>
}
</div>
</div>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-secondary me-2" @onclick="CloseAddOrderModal">Abbrechen</button>
<button type="submit" class="btn btn-success">Speichern</button>
</div>
</EditForm>
</div>
</div>
</div>
</div>
}
break;
case "bookings":
<div>
<h5>Buchungen</h5>
<p>Zeiterfassungen und Plan/Ist-Vergleiche werden hier erscheinen.</p>
</div>
break;
case "members":
<div>
<h5>Mitarbeiter</h5>
<p>Teamzuordnung, Rollenverwaltung, People Picker.</p>
</div>
break;
case "documents":
<div>
<h5>Dokumente</h5>
<p>Dateiupload, SharePoint-Verknüpfung, Projektreports.</p>
</div>
break;
case "comments":
<div>
<h5>Kommentare</h5>
<p>Chat/Kommentarbereich für Projektkommunikation.</p>
</div>
break;
}
</div>
}
@code {
[Parameter] public int id { get; set; }
private ProjectDto? project;
private string? errorMessage;
private bool isLoading = true;
// Tabs
private string activeTab = "overview";
private readonly List<(string Key, string Title, string Icon)> tabs = new()
{
("overview", "Übersicht", "bi bi-house"),
("tasks", "Aufträge", "bi bi-list-task"),
("bookings", "Buchungen", "bi bi-clock-history"),
("members", "Mitarbeiter", "bi bi-people"),
("documents", "Dokumente", "bi bi-folder2-open"),
("comments", "Kommentare", "bi bi-chat-left-text")
};
protected override async Task OnInitializedAsync()
{
await LoadProjectAsync();
var result = await OrderService.GetOrdersByProjectAsync(1);
Console.WriteLine($"Orders loaded: {result.Data?.Count}");
}
private async Task LoadProjectAsync()
{
try
{
var result = await ProjectService.GetProjectAsync(id);
if (!result.Success)
{
errorMessage = result.Error;
return;
}
project = result.Data;
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Laden: {ex.Message}";
}
finally
{
isLoading = false;
}
}
private string GetStatusColor(string? status) => status switch
{
"Geplant" => "secondary",
"In Arbeit" => "info",
"Abgeschlossen" => "success",
_ => "light"
};
private void SetActiveTab(string key) => activeTab = key;
private void GoBack() => Nav.NavigateTo("/projects");
private List<OrderDto>? orders;
private bool showAddOrderModal = false;
private OrderDto newOrder = new();
private List<UserDto> users = new();
private string searchText = "";
private List<int> selectedUserIds = new();
private IEnumerable<UserDto> FilteredUsers => string.IsNullOrWhiteSpace(searchText)
? users
: users.Where(u => $"{u.FirstName} {u.LastName} {u.Username}".Contains(searchText, StringComparison.OrdinalIgnoreCase));
private void ToggleUser(int id, bool selected)
{
if (selected)
{
selectedUserIds.Add(id);
}
else
{
selectedUserIds.Remove(id);
}
}
private void ShowAddOrderModal()
{
newOrder = new OrderDto
{
ProjectId = project!.Id,
Status = "Geplant",
MandantId = project.MandantId
};
selectedUserIds.Clear();
searchText = "";
showAddOrderModal = true;
}
private void CloseAddOrderModal() => showAddOrderModal = false;
private async Task LoadOrderAsync()
{
var result = await OrderService.GetOrdersByProjectAsync(project!.Id);
if(result.Success)
{
orders = result.Data;
}
else
{
errorMessage = result.Error;
}
}
private async Task CreateOrderAsync()
{
var result = await OrderService.CreateOrderAsync(newOrder);
if(!result.Success)
{
errorMessage = result.Error;
return;
}
CloseAddOrderModal();
await LoadOrderAsync();
}
private async Task DeleteOrder(int id)
{
var result = await OrderService.DeleteOrderAsync(id);
if (result.Success)
{
await LoadOrderAsync();
}
else
{
errorMessage = result.Error;
}
}
}
<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;
}
.modal-content {
border-radius: 0.75rem;
}
.list-group-item {
padding: 0.5rem 1rem;
}
</style>