497 lines
19 KiB
Plaintext
497 lines
19 KiB
Plaintext
@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>
|