Project Details Page

This commit is contained in:
Marc Wieland
2025-10-15 13:29:50 +02:00
parent 789d44a344
commit 910ed8b8f8
108 changed files with 3294 additions and 302 deletions

View File

View File

@@ -1,7 +1,106 @@
@page "/"
@inject NavigationManager Nav
<PageTitle>Home</PageTitle>
<h3 class="mb-4">Willkommen bei OnProf Next</h3>
<p class="text-muted">Wähle einen Bereich aus, um fortzufahren:</p>
<h1>Hello, world!</h1>
<div class="row g-4 mt-3">
<div class="col-12 col-sm-6 col-lg-3">
<div class="card dashboard-card text-center" @onclick="() => GoToUsers()">
<div class="card-body">
<i class="bi bi-people fs-1 text-primary"></i>
<h5 class="card-title mt-3">Benutzer</h5>
<p class="text-muted small">Verwalte Benutzer und Rechte.</p>
</div>
</div>
</div>
Welcome to your new app.
<div class="col-12 col-sm-6 col-lg-3">
<div class="card dashboard-card text-center" @onclick="() => GoToProjects()">
<div class="card-body">
<i class="bi bi-folder2-open fs-1 text-success"></i>
<h5 class="card-title mt-3">Projekte</h5>
<p class="text-muted small">Verwalte laufende Projekte und Aufträge.</p>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="card dashboard-card text-center" @onclick="() => GoToBookings()">
<div class="card-body">
<i class="bi bi-stopwatch fs-1 text-warning"></i>
<h5 class="card-title mt-3">Buchungen</h5>
<p class="text-muted small">Erfasse Arbeitszeiten auf Projekte.</p>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="card dashboard-card text-center" @onclick="() => GoToAnalysis()">
<div class="card-body">
<i class="bi bi-graph-up fs-1 text-danger"></i>
<h5 class="card-title mt-3">Auswertungen</h5>
<p class="text-muted small">Analysiere Projektzeiten und Fortschritt.</p>
</div>
</div>
</div>
<div class="col-12 col-sm-6 col-lg-3">
<div class="card dashboard-card text-center" @onclick="() => GoToMandants()">
<div class="card-body">
<i class="bi bi-graph-up fs-1 text-danger"></i>
<h5 class="card-title mt-3">Mandanten</h5>
<p class="text-muted small">Verwaltung von Mandanten</p>
</div>
</div>
</div>
</div>
@code{
private void GoToUsers()
{
Nav.NavigateTo("/users");
}
private void GoToProjects()
{
Nav.NavigateTo("/projects");
}
private void GoToBookings()
{
Nav.NavigateTo("/bookings");
}
private void GoToAnalysis()
{
Nav.NavigateTo("/analysis");
}
private void GoToMandants()
{
Nav.NavigateTo("/mandants");
}
}
<style>
.dashboard-card {
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-radius: 0.75rem;
}
.dashboard-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.card-body i {
transition: transform 0.3s ease;
}
.dashboard-card:hover i {
transform: scale(1.1);
}
</style>

View File

@@ -28,6 +28,11 @@
</div>
<button type="submit" class="btn btn-primary w-100">Login</button>
<div class="text-center mt-3">
<span>Noch keinen Account? </span>
<a href="/createuser" class="link-primary text-decoration-none">Registrieren</a>
</div>
</EditForm>
@@ -49,7 +54,7 @@
var result = await response.Content.ReadFromJsonAsync<LoginResponse>();
await AuthService.LoginAsync(result!.Token);
Nav.NavigateTo("/users");
Nav.NavigateTo("/");
}
catch
{

View File

@@ -0,0 +1,425 @@
@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>

View File

@@ -0,0 +1,331 @@
@page "/projects"
@using OnProfNext.Client.Services
@using OnProfNext.Shared.Models
@using OnProfNext.Shared.Models.DTOs
@inject ProjectApiService ProjectService
@inject UserApiService UserService
@inject NavigationManager Nav
<h3 class="mb-4 text-primary d-flex justify-content-between align-items-center">
<span>📁 Projektübersicht</span>
<button class="btn btn-success" @onclick="ShowAddModal">
<i class="bi bi-plus-circle"></i> Neues Projekt
</button>
</h3>
@if (errorMessage is not null)
{
<div class="alert alert-danger">@errorMessage</div>
}
else if (projects is null)
{
<div class="text-center mt-5">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-3 text-secondary">Projekte werden geladen...</p>
</div>
}
else if (!projects.Any())
{
<div class="alert alert-info text-center">
Es sind aktuell keine Projekte vorhanden.
</div>
}
else
{
@foreach (var group in projects.GroupBy(p => p.Status))
{
<h5 class="mt-4 mb-3 fw-bold text-secondary">
<i class="bi bi-kanban"></i> @group.Key
</h5>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-3">
@foreach (var p in group)
{
<div class="col">
<div class="card shadow-sm h-100 border-0 position-relative card-hover"
style="cursor: pointer;" @onclick="@(() => OpenProjectDetails(p.Id))"
>
<button class="btn btn-sm btn-outline-danger position-absolute top-0 end-0 m-2"
title="Projekt löschen"
@onclick="() => ConfirmDelete(p)">
<i class="bi bi-trash"></i>
</button>
<div class="card-body">
<h5 class="card-title text-primary">@p.ProjectName</h5>
@if (p.ProjectManagers?.Any() == true)
{
<div class="mb-2">
@foreach (var manager in p.ProjectManagers)
{
<span class="badge bg-primary me-1">@manager.FullName</span>
}
</div>
}
else
{
<p class="text-muted small mb-2"><i class="bi bi-person-circle"></i> Keine Projektleiter</p>
}
<p class="card-text">@p.Description</p>
</div>
<div class="card-footer bg-white border-0 text-end small text-muted">
<i class="bi bi-calendar"></i>
@p.StartDate.ToString("dd.MM.yyyy")
@if (p.EndDate is not null)
{
<span> @p.EndDate?.ToString("dd.MM.yyyy")</span>
}
</div>
</div>
</div>
}
</div>
}
}
<!-- Neues Projekt Modal -->
@if (showAddModal)
{
<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">Neues Projekt anlegen</h5>
<button type="button" class="btn-close btn-close-white" @onclick="CloseAddModal"></button>
</div>
<div class="modal-body">
<EditForm Model="newProject" OnValidSubmit="CreateProjectAsync">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label class="form-label">Projektname</label>
<InputText class="form-control" @bind-Value="newProject.ProjectName" />
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<InputTextArea class="form-control" rows="3" @bind-Value="newProject.Description" />
</div>
<div class="mb-3">
<label class="form-label">Projektleiter</label>
<InputText class="form-control mb-2" @bind-Value="searchText" placeholder="Projektleiter suchen..." />
<div class="list-group" style="max-height: 150px; overflow-y: auto;">
@if (!FilteredUsers.Any())
{
<div class="list-group-item text-muted">Keine Benutzer gefunden</div>
}
else
{
@foreach (var user in FilteredUsers)
{
<label class="list-group-item">
<input type="checkbox"
value="@user.Id"
checked="@(newProject.ProjectManagerIds.Contains(user.Id))"
@onchange="@(e => ToggleProjectManager(user.Id, e.Value != null && (bool)e.Value))" />
@user.FirstName @user.LastName (@user.Username)
</label>
}
}
</div>
</div>
<div class="mb-3">
<label class="form-label">Status</label>
<InputSelect class="form-select" @bind-Value="newProject.Status">
<option value="Geplant">Geplant</option>
<option value="In Arbeit">In Arbeit</option>
<option value="Abgeschlossen">Abgeschlossen</option>
</InputSelect>
</div>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-secondary me-2" @onclick="CloseAddModal">Abbrechen</button>
<button type="submit" class="btn btn-success">Speichern</button>
</div>
</EditForm>
</div>
</div>
</div>
</div>
}
@if (showDeleteModal)
{
<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-danger text-white">
<h5 class="modal-title">Projekt löschen</h5>
<button type="button" class="btn-close btn-close-white" @onclick="CloseDeleteModal"></button>
</div>
<div class="modal-body">
<p>Möchten Sie das Projekt <strong>@projectToDelete?.ProjectName</strong> wirklich löschen?</p>
<p class="text-muted small">Dieser Vorgang kann nicht rückgängig gemacht werden.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" @onclick="CloseDeleteModal">Abbrechen</button>
<button class="btn btn-danger" @onclick="DeleteProjectAsync">Löschen</button>
</div>
</div>
</div>
</div>
}
@code {
private List<ProjectDto>? projects;
private string? errorMessage;
private bool showAddModal = false;
private ProjectCreateDto newProject = new();
private List<User> users = new();
private string searchText = "";
private IEnumerable<User> FilteredUsers => string.IsNullOrWhiteSpace(searchText)
? users
: users.Where(u => $"{u.FirstName} {u.LastName} {u.Username}"
.Contains(searchText, StringComparison.OrdinalIgnoreCase));
protected override async Task OnInitializedAsync()
{
await LoadProjectsAsync();
var userResult = await UserService.GetUsersAsync();
if (userResult.Success && userResult.Data != null)
users = userResult.Data;
}
private async Task LoadProjectsAsync()
{
var result = await ProjectService.GetProjectsAsync();
if (!result.Success)
{
errorMessage = result.Error;
return;
}
projects = result.Data?
.OrderBy(p => p.Status)
.ThenByDescending(p => p.StartDate)
.ToList();
}
private void ShowAddModal()
{
newProject = new ProjectCreateDto { Status = "Geplant", MandantId = 1 };
searchText = "";
showAddModal = true;
}
private void CloseAddModal()
{
showAddModal = false;
}
private void ToggleProjectManager(int userId, bool isChecked)
{
if (isChecked)
{
if (!newProject.ProjectManagerIds.Contains(userId))
newProject.ProjectManagerIds.Add(userId);
}
else
{
newProject.ProjectManagerIds.Remove(userId);
}
}
private async Task CreateProjectAsync()
{
var result = await ProjectService.CreateProjectAsync(newProject);
if (!result.Success)
{
errorMessage = result.Error;
return;
}
CloseAddModal();
await LoadProjectsAsync();
}
private bool showDeleteModal = false;
private ProjectDto? projectToDelete;
private void ConfirmDelete(ProjectDto project)
{
projectToDelete = project;
showDeleteModal = true;
}
private void CloseDeleteModal()
{
showDeleteModal = false;
projectToDelete = null;
}
private async Task DeleteProjectAsync()
{
if(projectToDelete is null)
return;
var result = await ProjectService.DeleteProjectAsync(projectToDelete.Id);
if(!result.Success)
{
errorMessage = result.Error;
return;
}
CloseDeleteModal();
await LoadProjectsAsync();
}
private void OpenProjectDetails(int projectId)
{
Nav.NavigateTo($"/projects/{projectId}");
}
}
<style>
.card {
transition: all 0.15s ease-in-out;
border-radius: 0.75rem;
}
.card:hover {
transform: scale(1.02);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-content {
border-radius: 0.75rem;
}
.modal-header {
border-bottom: none;
}
.list-group-item {
padding: 0.5rem 1rem;
}
.list-group-item input[type="checkbox"] {
margin-right: 0.5rem;
}
.btn-outline-danger {
border: none;
color: #dc3545;
}
.btn-outline-danger:hover {
color: white;
background-color: #dc3545;
}
</style>