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

331 lines
12 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"
@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<UserDto> users = new();
private string searchText = "";
private IEnumerable<UserDto> 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>