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

497 lines
19 KiB
Plaintext
Raw Permalink 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>
@if (o.Mitarbeiter?.Any() == true)
{
@foreach (var m in o.Mitarbeiter)
{
<span class="badge bg-info text-dark me-1">@m.FirstName @m.LastName</span>
}
}
else
{
<span class="text-muted small">keine zugewiesen</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>
@if (projectMembers == null || !projectMembers.Any())
{
<div class="alert alert-info mt-3 text-center">
Keine Mitarbeiter zugewiesen.
</div>
}
else
{
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3 mt-3">
@foreach (var member in projectMembers)
{
<div class="col">
<div class="card h-100 shadow-sm border-0">
<div class="card-body">
<h6 class="card-title mb-1 text-primary">
@member.FirstName @member.LastName
</h6>
<p class="card-text text-muted small mb-1">@member.Username</p>
<p class="card-text text-muted small mb-0">
<i class="bi bi-envelope"></i> @member.Email
</p>
</div>
</div>
</div>
}
</div>
}
</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();
await LoadOrderAsync();
await LoadUsersAsync();
}
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;
RefreshProjectMembers();
}
else
{
errorMessage = result.Error;
}
}
private async Task CreateOrderAsync()
{
// Mitarbeiter mit reinpacken
newOrder.Mitarbeiter = users.Where(u => selectedUserIds.Contains(u.Id)).ToList();
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;
}
}
private async Task LoadUsersAsync()
{
var result = await UserService.GetUsersAsync();
if (result.Success && result.Data != null)
{
users = result.Data;
Console.WriteLine($"Loaded {users.Count} users.");
}
else
{
errorMessage = result.Error ?? "Fehler beim Laden der Benutzer.";
}
}
private List<UserDto> projectMembers = new();
private void RefreshProjectMembers()
{
if (orders == null) return;
projectMembers = orders
.Where(o => o.Mitarbeiter != null)
.SelectMany(o => o.Mitarbeiter!)
.GroupBy(u => u.Id)
.Select(g => g.First())
.OrderBy(u => u.LastName)
.ThenBy(u => u.FirstName)
.ToList();
}
}
<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>