Add employee-to-order assignments with FR-EM-3 conflict validation
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 17s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 6s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 17s
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 17s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 6s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 17s
Adds the Assignment core object (Order x Employee, date range) with full CRUD, soft-delete/trash integration, and generated API clients. Layers FR-EM-3 conflict checks onto assignment creation: qualification, absence/availability, working-hours approximation, cross-order overlap, and active-contract coverage. Each check's severity (Warning vs. Error) is configurable at runtime via a new AssignmentValidationSettings singleton and admin settings panel, instead of being hardcoded - lets the business tune strictness per check without a redeploy. Adds a live GET /api/assignments/check endpoint so the create-assignment dialog can preview conflicts as the user picks employee/dates, before they hit save, rather than only finding out after submitting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
fcbf27db3c
commit
dfeb37cf33
@@ -0,0 +1,3 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record AssignmentConflictResponse(string Type, string Severity, string Message);
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record AssignmentResponse(
|
||||
Guid Id,
|
||||
Guid OrderId,
|
||||
Guid EmployeeId,
|
||||
string EmployeeFirstName,
|
||||
string EmployeeLastName,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
string? Note,
|
||||
IReadOnlyList<AssignmentConflictResponse> Conflicts);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record AssignmentValidationSettingsResponse(
|
||||
string QualificationMode,
|
||||
string AbsenceMode,
|
||||
string WorkingHoursMode,
|
||||
string OverlapMode,
|
||||
string ContractMode);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record CreateAssignmentRequest(
|
||||
Guid OrderId,
|
||||
Guid EmployeeId,
|
||||
DateOnly StartDate,
|
||||
DateOnly EndDate,
|
||||
string? Note = null);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record TrashAssignmentResponse(
|
||||
Guid Id,
|
||||
string EmployeeFirstName,
|
||||
string EmployeeLastName,
|
||||
DateTime? DeletedAt);
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateAssignmentRequest(string? Note = null);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateAssignmentValidationSettingsRequest(
|
||||
string QualificationMode,
|
||||
string AbsenceMode,
|
||||
string WorkingHoursMode,
|
||||
string OverlapMode,
|
||||
string ContractMode);
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OmsorgCore.Api.Contracts;
|
||||
using OmsorgCore.Api.Security;
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Admin-Konfiguration der FR-EM-3-Konfliktprüfungen bei der Mitarbeiterzuweisung: pro Prüfung
|
||||
/// (Qualifikation/Abwesenheit/Arbeitszeit/Überschneidung/Vertrag) einzeln als Warning oder Error
|
||||
/// einstellbar, siehe AssignmentService.CreateAsync.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/settings/assignment-validation")]
|
||||
public class AssignmentValidationSettingsController : ControllerBase
|
||||
{
|
||||
private readonly IAssignmentValidationSettingsService _service;
|
||||
|
||||
public AssignmentValidationSettingsController(IAssignmentValidationSettingsService service)
|
||||
{
|
||||
_service = service;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.Configuration, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<AssignmentValidationSettingsResponse>> Get(CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _service.GetAsync(cancellationToken);
|
||||
return Ok(new AssignmentValidationSettingsResponse(
|
||||
settings.QualificationMode.ToString(),
|
||||
settings.AbsenceMode.ToString(),
|
||||
settings.WorkingHoursMode.ToString(),
|
||||
settings.OverlapMode.ToString(),
|
||||
settings.ContractMode.ToString()));
|
||||
}
|
||||
|
||||
[HttpPut]
|
||||
[RequirePermission(ModuleType.Configuration, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<AssignmentValidationSettingsResponse>> Update(
|
||||
UpdateAssignmentValidationSettingsRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryParseSeverity(request.QualificationMode, out var qualificationMode) ||
|
||||
!TryParseSeverity(request.AbsenceMode, out var absenceMode) ||
|
||||
!TryParseSeverity(request.WorkingHoursMode, out var workingHoursMode) ||
|
||||
!TryParseSeverity(request.OverlapMode, out var overlapMode) ||
|
||||
!TryParseSeverity(request.ContractMode, out var contractMode))
|
||||
{
|
||||
return BadRequest(new { error = "invalid_severity", message = "Mode must be \"Warning\" or \"Error\"" });
|
||||
}
|
||||
|
||||
var settings = await _service.UpdateAsync(qualificationMode, absenceMode, workingHoursMode, overlapMode, contractMode, cancellationToken);
|
||||
return Ok(new AssignmentValidationSettingsResponse(
|
||||
settings.QualificationMode.ToString(),
|
||||
settings.AbsenceMode.ToString(),
|
||||
settings.WorkingHoursMode.ToString(),
|
||||
settings.OverlapMode.ToString(),
|
||||
settings.ContractMode.ToString()));
|
||||
}
|
||||
|
||||
private static bool TryParseSeverity(string value, out ValidationSeverity severity)
|
||||
=> Enum.TryParse(value, ignoreCase: true, out severity);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OmsorgCore.Api.Contracts;
|
||||
using OmsorgCore.Api.Security;
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/assignments")]
|
||||
public class AssignmentsController : ControllerBase
|
||||
{
|
||||
private readonly IAssignmentService _assignmentService;
|
||||
|
||||
public AssignmentsController(IAssignmentService assignmentService)
|
||||
{
|
||||
_assignmentService = assignmentService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.View)]
|
||||
public async Task<ActionResult<PagedResponse<AssignmentResponse>>> GetAll(
|
||||
[FromQuery] Guid? orderId = null,
|
||||
[FromQuery] Guid? employeeId = null,
|
||||
[FromQuery] DateOnly? fromDate = null,
|
||||
[FromQuery] DateOnly? toDate = null,
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
page = Math.Max(page, 1);
|
||||
pageSize = Math.Clamp(pageSize, 1, 100);
|
||||
|
||||
var (items, totalCount) = await _assignmentService.GetPagedAsync(
|
||||
orderId, employeeId, fromDate, toDate, page, pageSize, cancellationToken);
|
||||
var responses = items.Select(a => ToResponse(a)).ToList();
|
||||
return Ok(new PagedResponse<AssignmentResponse>(responses, totalCount, page, pageSize));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Läuft dieselben FR-EM-3-Konfliktprüfungen wie <see cref="Create"/>, ohne etwas anzulegen -
|
||||
/// für die Live-Vorschau im Zuweisungs-Dialog, sobald Mitarbeiter/Zeitraum ausgewählt sind.
|
||||
/// </summary>
|
||||
[HttpGet("check")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Create)]
|
||||
public async Task<ActionResult<IReadOnlyList<AssignmentConflictResponse>>> Check(
|
||||
[FromQuery] Guid orderId,
|
||||
[FromQuery] Guid employeeId,
|
||||
[FromQuery] DateOnly startDate,
|
||||
[FromQuery] DateOnly endDate,
|
||||
[FromQuery] Guid? excludeAssignmentId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var conflicts = await _assignmentService.CheckConflictsAsync(
|
||||
orderId, employeeId, startDate, endDate, excludeAssignmentId, cancellationToken);
|
||||
return Ok(conflicts.Select(ToConflictResponse).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.View)]
|
||||
public async Task<ActionResult<AssignmentResponse>> GetById(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var assignment = await _assignmentService.GetByIdAsync(id, cancellationToken);
|
||||
return assignment is null ? NotFound() : Ok(ToResponse(assignment));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Create)]
|
||||
public async Task<ActionResult<AssignmentResponse>> Create(
|
||||
CreateAssignmentRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _assignmentService.CreateAsync(
|
||||
request.OrderId,
|
||||
request.EmployeeId,
|
||||
request.StartDate,
|
||||
request.EndDate,
|
||||
request.Note,
|
||||
cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
return result.Reason switch
|
||||
{
|
||||
CreateAssignmentFailureReason.OrderNotFound => NotFound(new { error = "order_not_found" }),
|
||||
CreateAssignmentFailureReason.EmployeeNotFound => NotFound(new { error = "employee_not_found" }),
|
||||
CreateAssignmentFailureReason.InvalidDateRange => BadRequest(new { error = "invalid_date_range", message = "End date must be >= start date" }),
|
||||
CreateAssignmentFailureReason.DateOutsideOrderRange => BadRequest(new { error = "date_outside_order_range", message = "Assignment dates must be within the order's date range" }),
|
||||
CreateAssignmentFailureReason.OverlappingAssignment => Conflict(new { error = "overlapping_assignment", message = "Employee already has an assignment in this time range for this order" }),
|
||||
CreateAssignmentFailureReason.ValidationConflict => Conflict(new { error = "validation_conflict", conflicts = result.Conflicts.Select(ToConflictResponse) }),
|
||||
_ => BadRequest(new { error = "unknown_error" })
|
||||
};
|
||||
}
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { id = result.Assignment!.Id }, ToResponse(result.Assignment, result.Conflicts));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<AssignmentResponse>> Update(
|
||||
Guid id,
|
||||
UpdateAssignmentRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var assignment = await _assignmentService.UpdateAsync(id, request.Note, cancellationToken);
|
||||
return assignment is null ? NotFound() : Ok(ToResponse(assignment));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Delete)]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var deleted = await _assignmentService.DeleteAsync(id, cancellationToken);
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
private static AssignmentResponse ToResponse(Assignment assignment, IReadOnlyList<AssignmentConflict>? conflicts = null)
|
||||
=> new(
|
||||
assignment.Id,
|
||||
assignment.OrderId,
|
||||
assignment.EmployeeId,
|
||||
assignment.Employee.FirstName,
|
||||
assignment.Employee.LastName,
|
||||
assignment.StartDate,
|
||||
assignment.EndDate,
|
||||
assignment.Note,
|
||||
(conflicts ?? Array.Empty<AssignmentConflict>()).Select(ToConflictResponse).ToList());
|
||||
|
||||
private static AssignmentConflictResponse ToConflictResponse(AssignmentConflict conflict)
|
||||
=> new(conflict.Type.ToString(), conflict.Severity.ToString(), conflict.Message);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public class TrashController : ControllerBase
|
||||
private readonly IEmployeeFacilityDistanceService _employeeFacilityDistanceService;
|
||||
private readonly IAbsenceService _absenceService;
|
||||
private readonly ITimeEntryService _timeEntryService;
|
||||
private readonly IAssignmentService _assignmentService;
|
||||
|
||||
public TrashController(
|
||||
IEmployeeService employeeService,
|
||||
@@ -39,7 +40,8 @@ public class TrashController : ControllerBase
|
||||
IFacilityQualificationRateService facilityQualificationRateService,
|
||||
IEmployeeFacilityDistanceService employeeFacilityDistanceService,
|
||||
IAbsenceService absenceService,
|
||||
ITimeEntryService timeEntryService)
|
||||
ITimeEntryService timeEntryService,
|
||||
IAssignmentService assignmentService)
|
||||
{
|
||||
_employeeService = employeeService;
|
||||
_facilityService = facilityService;
|
||||
@@ -50,6 +52,7 @@ public class TrashController : ControllerBase
|
||||
_employeeFacilityDistanceService = employeeFacilityDistanceService;
|
||||
_absenceService = absenceService;
|
||||
_timeEntryService = timeEntryService;
|
||||
_assignmentService = assignmentService;
|
||||
}
|
||||
|
||||
[HttpGet("employees")]
|
||||
@@ -195,4 +198,20 @@ public class TrashController : ControllerBase
|
||||
var restored = await _timeEntryService.RestoreAsync(id, cancellationToken);
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("assignments")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Recover)]
|
||||
public async Task<ActionResult<IReadOnlyList<TrashAssignmentResponse>>> GetDeletedAssignments([FromQuery] string? search, CancellationToken cancellationToken)
|
||||
{
|
||||
var assignments = await _assignmentService.GetDeletedAsync(search, cancellationToken);
|
||||
return Ok(assignments.Select(a => new TrashAssignmentResponse(a.Id, a.Employee.FirstName, a.Employee.LastName, a.DeletedAt)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("assignments/{id:guid}/restore")]
|
||||
[RequirePermission(ModuleType.Assignments, PermissionAction.Recover)]
|
||||
public async Task<IActionResult> RestoreAssignment(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var restored = await _assignmentService.RestoreAsync(id, cancellationToken);
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@ public interface IAbsenceRepository
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default,
|
||||
Guid? restrictToEmployeeId = null);
|
||||
/// <summary>
|
||||
/// Prüft, ob eine nicht gelöschte Abwesenheit der Mitarbeiterin mit einem Status aus
|
||||
/// <paramref name="blockingStatusValues"/> (siehe ValueListItem.BlocksAssignment) den Zeitraum
|
||||
/// [startDate, endDate] überlappt - Basis der Abwesenheitsprüfung in FR-EM-3.
|
||||
/// </summary>
|
||||
Task<bool> HasOverlappingAbsenceAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
IReadOnlyCollection<string> blockingStatusValues,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(Absence absence, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Absence absence, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
namespace OmsorgCore.Application.Abstractions;
|
||||
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public interface IAssignmentRepository
|
||||
{
|
||||
Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Assignment>> GetAllAsync(CancellationToken cancellationToken = default);
|
||||
Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> HasOverlapAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Zählt die Tage, an denen die Mitarbeiterin innerhalb von [weekStart, weekEnd] bereits
|
||||
/// eingeteilt ist (bestehende Zuweisungen, ohne excludeAssignmentId) - Basis für die
|
||||
/// Arbeitszeit-Näherung in FR-EM-3 (8h/Tag gegen Contract.WeeklyHours).
|
||||
/// </summary>
|
||||
Task<int> GetAssignedDayCountInWeekAsync(
|
||||
Guid employeeId,
|
||||
DateOnly weekStart,
|
||||
DateOnly weekEnd,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<int> CountForOrderAsync(Guid orderId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(Assignment assignment, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Assignment assignment, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default);
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Abstractions;
|
||||
|
||||
public interface IAssignmentValidationSettingsRepository
|
||||
{
|
||||
/// <summary>Liefert die eine Settings-Zeile, legt sie mit den Entity-Defaults an, falls noch keine existiert.</summary>
|
||||
Task<AssignmentValidationSettings> GetOrCreateAsync(CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(AssignmentValidationSettings settings, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -14,6 +14,17 @@ public interface IContractRepository
|
||||
int page,
|
||||
int pageSize,
|
||||
CancellationToken cancellationToken = default);
|
||||
/// <summary>
|
||||
/// Liefert den ersten aktiven Vertrag (Status != "Entwurf") der Mitarbeiterin, dessen Zeitraum
|
||||
/// [startDate, endDate] vollständig abdeckt, oder null - Basis der Vertrags- und der
|
||||
/// Arbeitszeit-Prüfung (WeeklyHours) in FR-EM-3.
|
||||
/// </summary>
|
||||
Task<Contract?> GetActiveForEmployeeCoveringRangeAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task AddAsync(Contract contract, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(Contract contract, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -17,7 +17,9 @@ public static class DependencyInjection
|
||||
services.AddScoped<IEmployeeFacilityDistanceService, EmployeeFacilityDistanceService>();
|
||||
services.AddScoped<IContractService, ContractService>();
|
||||
services.AddScoped<IOrderService, OrderService>();
|
||||
services.AddScoped<IAssignmentService, AssignmentService>();
|
||||
services.AddScoped<IAbsenceService, AbsenceService>();
|
||||
services.AddScoped<IAssignmentValidationSettingsService, AssignmentValidationSettingsService>();
|
||||
services.AddScoped<ITimeEntryService, TimeEntryService>();
|
||||
services.AddScoped<IDocumentService, DocumentService>();
|
||||
services.AddScoped<IValueListService, ValueListService>();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public enum AssignmentConflictType { Qualification, Absence, WorkingHours, Overlap, Contract }
|
||||
|
||||
public record AssignmentConflict(AssignmentConflictType Type, ValidationSeverity Severity, string Message);
|
||||
@@ -0,0 +1,290 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
public class AssignmentService : IAssignmentService
|
||||
{
|
||||
private const int AssumedHoursPerDay = 8;
|
||||
|
||||
private readonly IAssignmentRepository _assignmentRepository;
|
||||
private readonly IOrderRepository _orderRepository;
|
||||
private readonly IEmployeeRepository _employeeRepository;
|
||||
private readonly IAbsenceRepository _absenceRepository;
|
||||
private readonly IContractRepository _contractRepository;
|
||||
private readonly IValueListRepository _valueListRepository;
|
||||
private readonly IAssignmentValidationSettingsRepository _validationSettingsRepository;
|
||||
|
||||
public AssignmentService(
|
||||
IAssignmentRepository assignmentRepository,
|
||||
IOrderRepository orderRepository,
|
||||
IEmployeeRepository employeeRepository,
|
||||
IAbsenceRepository absenceRepository,
|
||||
IContractRepository contractRepository,
|
||||
IValueListRepository valueListRepository,
|
||||
IAssignmentValidationSettingsRepository validationSettingsRepository)
|
||||
{
|
||||
_assignmentRepository = assignmentRepository;
|
||||
_orderRepository = orderRepository;
|
||||
_employeeRepository = employeeRepository;
|
||||
_absenceRepository = absenceRepository;
|
||||
_contractRepository = contractRepository;
|
||||
_valueListRepository = valueListRepository;
|
||||
_validationSettingsRepository = validationSettingsRepository;
|
||||
}
|
||||
|
||||
public async Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _assignmentRepository.GetPagedAsync(
|
||||
orderId, employeeId, fromDate, toDate, page, pageSize, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _assignmentRepository.GetByIdAsync(id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<CreateAssignmentResult> CreateAsync(
|
||||
Guid orderId,
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
string? note,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Validate Order exists
|
||||
var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken);
|
||||
if (order is null)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.OrderNotFound, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
// Validate Employee exists
|
||||
var employee = await _employeeRepository.GetByIdAsync(employeeId, cancellationToken);
|
||||
if (employee is null)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.EmployeeNotFound, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
// Validate date range
|
||||
if (endDate < startDate)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.InvalidDateRange, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
// Validate dates are within Order's range
|
||||
var orderEndDate = order.EndDate ?? DateOnly.MaxValue;
|
||||
if (startDate < order.StartDate || endDate > orderEndDate)
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.DateOutsideOrderRange, null, Array.Empty<AssignmentConflict>());
|
||||
}
|
||||
|
||||
var conflicts = await CheckConflictsAsync(order, employee, startDate, endDate, excludeAssignmentId: null, cancellationToken);
|
||||
if (conflicts.Any(c => c.Severity == ValidationSeverity.Error))
|
||||
{
|
||||
return new CreateAssignmentResult(false, CreateAssignmentFailureReason.ValidationConflict, null, conflicts);
|
||||
}
|
||||
|
||||
// Create assignment
|
||||
var assignment = new Assignment
|
||||
{
|
||||
OrderId = orderId,
|
||||
EmployeeId = employeeId,
|
||||
StartDate = startDate,
|
||||
EndDate = endDate,
|
||||
Note = note
|
||||
};
|
||||
|
||||
await _assignmentRepository.AddAsync(assignment, cancellationToken);
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Reload to include the Employee navigation for the result
|
||||
var reloaded = await _assignmentRepository.GetByIdAsync(assignment.Id, cancellationToken);
|
||||
return new CreateAssignmentResult(true, CreateAssignmentFailureReason.None, reloaded, conflicts);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
|
||||
Guid orderId,
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (endDate < startDate)
|
||||
{
|
||||
return Array.Empty<AssignmentConflict>();
|
||||
}
|
||||
|
||||
var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken);
|
||||
var employee = await _employeeRepository.GetByIdAsync(employeeId, cancellationToken);
|
||||
if (order is null || employee is null)
|
||||
{
|
||||
return Array.Empty<AssignmentConflict>();
|
||||
}
|
||||
|
||||
return await CheckConflictsAsync(order, employee, startDate, endDate, excludeAssignmentId, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
|
||||
Order order,
|
||||
Employee employee,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var settings = await _validationSettingsRepository.GetOrCreateAsync(cancellationToken);
|
||||
var conflicts = new List<AssignmentConflict>();
|
||||
|
||||
// Qualifikation: Employee.Qualification muss mindestens den Rang von Order.RequiredQualification haben
|
||||
// (SortOrder der gemeinsamen ValueList "Qualification", siehe omsorgCore/CLAUDE.md "Konfigurierbare Auswahllisten").
|
||||
if (!string.IsNullOrWhiteSpace(order.RequiredQualification))
|
||||
{
|
||||
var qualificationItems = await _valueListRepository.GetItemsAsync("Qualification", cancellationToken);
|
||||
var requiredItem = qualificationItems.FirstOrDefault(i => i.Value == order.RequiredQualification);
|
||||
var employeeItem = qualificationItems.FirstOrDefault(i => i.Value == employee.Qualification);
|
||||
|
||||
if (requiredItem is not null && (employeeItem is null || employeeItem.SortOrder < requiredItem.SortOrder))
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Qualification,
|
||||
settings.QualificationMode,
|
||||
$"Mitarbeiterin erfüllt die benötigte Qualifikation \"{order.RequiredQualification}\" nicht (aktuell: \"{employee.Qualification ?? "keine"}\")."));
|
||||
}
|
||||
}
|
||||
|
||||
// Abwesenheit (deckt "Verfügbarkeit" ab, siehe Plan): genehmigte Abwesenheit überlappt Zeitraum.
|
||||
var blockingAbsenceStatuses = (await _valueListRepository.GetItemsAsync("AbsenceStatus", cancellationToken))
|
||||
.Where(i => i.BlocksAssignment)
|
||||
.Select(i => i.Value)
|
||||
.ToList();
|
||||
|
||||
if (await _absenceRepository.HasOverlappingAbsenceAsync(employee.Id, startDate, endDate, blockingAbsenceStatuses, cancellationToken))
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Absence,
|
||||
settings.AbsenceMode,
|
||||
"Mitarbeiterin hat im Zuweisungszeitraum eine genehmigte Abwesenheit."));
|
||||
}
|
||||
|
||||
// Überschneidung: jetzt auftragsübergreifend geprüft (nicht mehr nur derselbe Auftrag).
|
||||
if (await _assignmentRepository.HasOverlapAsync(employee.Id, startDate, endDate, excludeAssignmentId, cancellationToken))
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Overlap,
|
||||
settings.OverlapMode,
|
||||
"Mitarbeiterin ist im Zuweisungszeitraum bereits einem anderen Auftrag zugewiesen."));
|
||||
}
|
||||
|
||||
// Vertrag + Arbeitszeit: beide brauchen den aktiven Vertrag der Mitarbeiterin für den Zeitraum.
|
||||
var activeContract = await _contractRepository.GetActiveForEmployeeCoveringRangeAsync(employee.Id, startDate, endDate, cancellationToken);
|
||||
|
||||
if (activeContract is null)
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.Contract,
|
||||
settings.ContractMode,
|
||||
"Mitarbeiterin hat keinen aktiven Vertrag, der den Zuweisungszeitraum abdeckt."));
|
||||
}
|
||||
else if (activeContract.WeeklyHours.HasValue)
|
||||
{
|
||||
var weeklyHourCap = activeContract.WeeklyHours.Value;
|
||||
var maxDaysPerWeek = (int)Math.Ceiling(weeklyHourCap / AssumedHoursPerDay);
|
||||
|
||||
var current = startDate;
|
||||
while (current <= endDate)
|
||||
{
|
||||
var weekStart = GetIsoWeekStart(current);
|
||||
var weekEnd = weekStart.AddDays(6);
|
||||
|
||||
var existingDays = await _assignmentRepository.GetAssignedDayCountInWeekAsync(
|
||||
employee.Id, weekStart, weekEnd, excludeAssignmentId, cancellationToken);
|
||||
|
||||
var newDaysInWeek = CountOverlapDays(startDate, endDate, weekStart, weekEnd);
|
||||
var totalDays = existingDays + newDaysInWeek;
|
||||
|
||||
if (totalDays > maxDaysPerWeek)
|
||||
{
|
||||
conflicts.Add(new AssignmentConflict(
|
||||
AssignmentConflictType.WorkingHours,
|
||||
settings.WorkingHoursMode,
|
||||
$"Arbeitszeit-Näherung (8h/Tag) überschreitet in der Woche ab {weekStart:yyyy-MM-dd} die Vertrags-Wochenstunden ({weeklyHourCap}h): {totalDays} zugewiesene Tage statt max. {maxDaysPerWeek}."));
|
||||
break;
|
||||
}
|
||||
|
||||
current = weekEnd.AddDays(1);
|
||||
}
|
||||
}
|
||||
|
||||
return conflicts;
|
||||
}
|
||||
|
||||
private static DateOnly GetIsoWeekStart(DateOnly date)
|
||||
{
|
||||
var daysSinceMonday = ((int)date.DayOfWeek + 6) % 7;
|
||||
return date.AddDays(-daysSinceMonday);
|
||||
}
|
||||
|
||||
private static int CountOverlapDays(DateOnly aStart, DateOnly aEnd, DateOnly bStart, DateOnly bEnd)
|
||||
{
|
||||
var rangeStart = aStart > bStart ? aStart : bStart;
|
||||
var rangeEnd = aEnd < bEnd ? aEnd : bEnd;
|
||||
return rangeEnd < rangeStart ? 0 : rangeEnd.DayNumber - rangeStart.DayNumber + 1;
|
||||
}
|
||||
|
||||
public async Task<Assignment?> UpdateAsync(
|
||||
Guid id,
|
||||
string? note,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assignment = await _assignmentRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (assignment is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
assignment.Note = note;
|
||||
|
||||
await _assignmentRepository.UpdateAsync(assignment, cancellationToken);
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return assignment;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var deleted = await _assignmentRepository.SoftDeleteAsync(id, cancellationToken);
|
||||
if (deleted)
|
||||
{
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _assignmentRepository.GetDeletedAsync(search, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var restored = await _assignmentRepository.RestoreAsync(id, cancellationToken);
|
||||
if (restored)
|
||||
{
|
||||
await _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
return restored;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _assignmentRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public class AssignmentValidationSettingsService : IAssignmentValidationSettingsService
|
||||
{
|
||||
private readonly IAssignmentValidationSettingsRepository _repository;
|
||||
|
||||
public AssignmentValidationSettingsService(IAssignmentValidationSettingsRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public Task<AssignmentValidationSettings> GetAsync(CancellationToken cancellationToken = default)
|
||||
=> _repository.GetOrCreateAsync(cancellationToken);
|
||||
|
||||
public async Task<AssignmentValidationSettings> UpdateAsync(
|
||||
ValidationSeverity qualificationMode,
|
||||
ValidationSeverity absenceMode,
|
||||
ValidationSeverity workingHoursMode,
|
||||
ValidationSeverity overlapMode,
|
||||
ValidationSeverity contractMode,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = await _repository.GetOrCreateAsync(cancellationToken);
|
||||
|
||||
settings.QualificationMode = qualificationMode;
|
||||
settings.AbsenceMode = absenceMode;
|
||||
settings.WorkingHoursMode = workingHoursMode;
|
||||
settings.OverlapMode = overlapMode;
|
||||
settings.ContractMode = contractMode;
|
||||
|
||||
await _repository.UpdateAsync(settings, cancellationToken);
|
||||
await _repository.SaveChangesAsync(cancellationToken);
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public enum CreateAssignmentFailureReason { None, OrderNotFound, EmployeeNotFound, InvalidDateRange, DateOutsideOrderRange, OverlappingAssignment, ValidationConflict }
|
||||
|
||||
public record CreateAssignmentResult(
|
||||
bool Success,
|
||||
CreateAssignmentFailureReason Reason,
|
||||
Assignment? Assignment,
|
||||
IReadOnlyList<AssignmentConflict> Conflicts);
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public interface IAssignmentService
|
||||
{
|
||||
Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<CreateAssignmentResult> CreateAsync(Guid orderId, Guid employeeId, DateOnly startDate, DateOnly endDate, string? note, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Führt dieselben FR-EM-3-Konfliktprüfungen wie <see cref="CreateAsync"/> aus, ohne eine
|
||||
/// Zuweisung anzulegen - für die Live-Vorschau im Anlegen-/Bearbeiten-Dialog. Liefert eine
|
||||
/// leere Liste, wenn Auftrag/Mitarbeiter (noch) nicht existieren oder der Zeitraum ungültig ist
|
||||
/// (diese Fälle werden im Formular bereits über Pflichtfelder/Min-Max abgefangen).
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<AssignmentConflict>> CheckConflictsAsync(
|
||||
Guid orderId,
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Assignment?> UpdateAsync(Guid id, string? note, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search = null, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public interface IAssignmentValidationSettingsService
|
||||
{
|
||||
Task<AssignmentValidationSettings> GetAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<AssignmentValidationSettings> UpdateAsync(
|
||||
ValidationSeverity qualificationMode,
|
||||
ValidationSeverity absenceMode,
|
||||
ValidationSeverity workingHoursMode,
|
||||
ValidationSeverity overlapMode,
|
||||
ValidationSeverity contractMode,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace OmsorgCore.Domain.Entities;
|
||||
|
||||
using OmsorgCore.Domain.Common;
|
||||
|
||||
public class Assignment : AuditableEntity
|
||||
{
|
||||
public Guid OrderId { get; set; }
|
||||
public Order Order { get; set; } = null!;
|
||||
|
||||
public Guid EmployeeId { get; set; }
|
||||
public Employee Employee { get; set; } = null!;
|
||||
|
||||
public DateOnly StartDate { get; set; }
|
||||
public DateOnly EndDate { get; set; }
|
||||
public string? Note { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using OmsorgCore.Domain.Common;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Systemweite, zur Laufzeit änderbare Konfiguration der FR-EM-3-Konfliktprüfungen bei der
|
||||
/// Mitarbeiterzuweisung (<see cref="Assignment"/>). Genau eine Zeile (Singleton) —
|
||||
/// <see cref="Application.Abstractions.IAssignmentValidationSettingsRepository.GetOrCreateAsync"/>
|
||||
/// legt sie bei Bedarf mit den hier hinterlegten Defaults an.
|
||||
/// </summary>
|
||||
public class AssignmentValidationSettings : Entity
|
||||
{
|
||||
public ValidationSeverity QualificationMode { get; set; } = ValidationSeverity.Error;
|
||||
public ValidationSeverity AbsenceMode { get; set; } = ValidationSeverity.Error;
|
||||
public ValidationSeverity WorkingHoursMode { get; set; } = ValidationSeverity.Warning;
|
||||
public ValidationSeverity OverlapMode { get; set; } = ValidationSeverity.Error;
|
||||
public ValidationSeverity ContractMode { get; set; } = ValidationSeverity.Error;
|
||||
}
|
||||
@@ -23,4 +23,11 @@ public class ValueListItem : Entity
|
||||
public bool IsTerminal { get; set; }
|
||||
public bool TriggersFollowUp { get; set; }
|
||||
public bool IsEditableByOwner { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nur für "AbsenceStatus" relevant: markiert den Status-Wert (aktuell "Genehmigt"), der eine
|
||||
/// Mitarbeiterzuweisung im überlappenden Zeitraum blockiert/warnt (FR-EM-3, Abwesenheitsprüfung).
|
||||
/// Bei allen anderen Listen bleibt es <c>false</c> — analog zu <see cref="IsInitial"/>/<see cref="IsTerminal"/>.
|
||||
/// </summary>
|
||||
public bool BlocksAssignment { get; set; }
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@ public enum ModuleType
|
||||
Users,
|
||||
Configuration,
|
||||
Absences,
|
||||
EmployeeFacilityDistances
|
||||
EmployeeFacilityDistances,
|
||||
Assignments
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace OmsorgCore.Domain.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Steuert, ob eine Zuweisungs-Konfliktprüfung (FR-EM-3) die Zuweisung blockiert (<see cref="Error"/>)
|
||||
/// oder nur als Hinweis mitgegeben wird, ohne sie zu verhindern (<see cref="Warning"/>).
|
||||
/// </summary>
|
||||
public enum ValidationSeverity
|
||||
{
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
@@ -38,7 +38,9 @@ public static class DependencyInjection
|
||||
services.AddScoped<IEmployeeFacilityDistanceRepository, EmployeeFacilityDistanceRepository>();
|
||||
services.AddScoped<IContractRepository, ContractRepository>();
|
||||
services.AddScoped<IOrderRepository, OrderRepository>();
|
||||
services.AddScoped<IAssignmentRepository, AssignmentRepository>();
|
||||
services.AddScoped<IAbsenceRepository, AbsenceRepository>();
|
||||
services.AddScoped<IAssignmentValidationSettingsRepository, AssignmentValidationSettingsRepository>();
|
||||
services.AddScoped<ITimeEntryRepository, TimeEntryRepository>();
|
||||
services.AddScoped<IDocumentRepository, DocumentRepository>();
|
||||
services.AddScoped<IDocumentStorage, FileSystemDocumentStorage>();
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Configurations;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
public class AssignmentConfiguration : IEntityTypeConfiguration<Assignment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Assignment> builder)
|
||||
{
|
||||
builder.ToTable("assignments");
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.HasOne(a => a.Order).WithMany().HasForeignKey(a => a.OrderId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.HasOne(a => a.Employee).WithMany().HasForeignKey(a => a.EmployeeId).OnDelete(DeleteBehavior.Restrict);
|
||||
builder.Property(a => a.Note).HasMaxLength(500);
|
||||
builder.HasQueryFilter(a => !a.IsDeleted);
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class AssignmentValidationSettingsConfiguration : IEntityTypeConfiguration<AssignmentValidationSettings>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AssignmentValidationSettings> builder)
|
||||
{
|
||||
builder.ToTable("assignment_validation_settings");
|
||||
builder.HasKey(s => s.Id);
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ public static class DbSeeder
|
||||
(ModuleType.Facilities, AllActions, PermissionScope.All),
|
||||
(ModuleType.Contracts, AllActions, PermissionScope.All),
|
||||
(ModuleType.Orders, AllActions, PermissionScope.All),
|
||||
(ModuleType.Assignments, AllActions, PermissionScope.All),
|
||||
(ModuleType.TimeEntries, AllActions, PermissionScope.All),
|
||||
(ModuleType.Invoices, new[] { PermissionAction.View, PermissionAction.Create, PermissionAction.Approve }, PermissionScope.All),
|
||||
(ModuleType.Recruiting, new[] { PermissionAction.View, PermissionAction.Create, PermissionAction.Edit }, PermissionScope.All),
|
||||
@@ -107,6 +108,7 @@ public static class DbSeeder
|
||||
{
|
||||
(ModuleType.Employees, new[] { PermissionAction.View }, PermissionScope.Own),
|
||||
(ModuleType.Contracts, new[] { PermissionAction.View }, PermissionScope.Own),
|
||||
(ModuleType.Assignments, new[] { PermissionAction.View }, PermissionScope.Own),
|
||||
(ModuleType.Absences, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own),
|
||||
(ModuleType.TimeEntries, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own),
|
||||
(ModuleType.EmployeeFacilityDistances, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own)
|
||||
@@ -204,6 +206,7 @@ public static class DbSeeder
|
||||
await SeedSimpleListIfMissingAsync(db, "AbsenceStatus", "Antragsstatus",
|
||||
new (string Value, bool IsDefault)[] { ("Eingereicht", true), ("Genehmigt", false), ("Abgelehnt", false) }, cancellationToken);
|
||||
await SeedAbsenceStatusInitialFlagIfMissingAsync(db, cancellationToken);
|
||||
await SeedAbsenceStatusBlocksAssignmentFlagIfMissingAsync(db, cancellationToken);
|
||||
|
||||
await SeedOrderStatusListIfMissingAsync(db, cancellationToken);
|
||||
await SeedCrmStatusTransitionsIfMissingAsync(db, cancellationToken);
|
||||
@@ -439,6 +442,29 @@ public static class DbSeeder
|
||||
item.IsInitial = true;
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
// "Genehmigt" ist der einzige AbsenceStatus-Wert, der eine Mitarbeiterzuweisung im
|
||||
// überlappenden Zeitraum blockiert/warnt (FR-EM-3, AssignmentService.CreateAsync liest
|
||||
// darüber statt den Anzeigetext hartzukodieren, siehe BlocksAssignment auf ValueListItem).
|
||||
// Idempotent wie SeedAbsenceStatusInitialFlagIfMissingAsync - läuft nicht erneut, sobald
|
||||
// irgendein Item der Liste bereits BlocksAssignment trägt.
|
||||
async Task SeedAbsenceStatusBlocksAssignmentFlagIfMissingAsync(OmsorgCoreDbContext context, CancellationToken ct)
|
||||
{
|
||||
if (await context.ValueListItems.AnyAsync(i => i.ValueList.Key == "AbsenceStatus" && i.BlocksAssignment, ct))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var item = await context.ValueListItems
|
||||
.FirstOrDefaultAsync(i => i.ValueList.Key == "AbsenceStatus" && i.Value == "Genehmigt", ct);
|
||||
if (item is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
item.BlocksAssignment = true;
|
||||
await context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedRoleIfMissingAsync(
|
||||
|
||||
+1453
File diff suppressed because it is too large
Load Diff
+64
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAssignmentEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
OrderId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
EmployeeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
StartDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
EndDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
Note = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
IsDeleted = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignments_employees_EmployeeId",
|
||||
column: x => x.EmployeeId,
|
||||
principalTable: "employees",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_assignments_orders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "orders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignments_EmployeeId",
|
||||
table: "assignments",
|
||||
column: "EmployeeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_assignments_OrderId",
|
||||
table: "assignments",
|
||||
column: "OrderId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignments");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1482
File diff suppressed because it is too large
Load Diff
+49
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAssignmentValidationSettingsAndBlocksAssignmentFlag : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "BlocksAssignment",
|
||||
table: "value_list_items",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "assignment_validation_settings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
QualificationMode = table.Column<int>(type: "integer", nullable: false),
|
||||
AbsenceMode = table.Column<int>(type: "integer", nullable: false),
|
||||
WorkingHoursMode = table.Column<int>(type: "integer", nullable: false),
|
||||
OverlapMode = table.Column<int>(type: "integer", nullable: false),
|
||||
ContractMode = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_assignment_validation_settings", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "assignment_validation_settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BlocksAssignment",
|
||||
table: "value_list_items");
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -82,6 +82,75 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("absences", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Assignment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("EmployeeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateOnly>("EndDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<Guid>("OrderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateOnly>("StartDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmployeeId");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.ToTable("assignments", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.AssignmentValidationSettings", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AbsenceMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ContractMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("OverlapMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("QualificationMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("WorkingHoursMode")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("assignment_validation_settings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.AuditLogEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -1064,6 +1133,9 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("BlocksAssignment")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDefault")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
@@ -1136,6 +1208,25 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Employee");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Assignment", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Employee", "Employee")
|
||||
.WithMany()
|
||||
.HasForeignKey("EmployeeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Order", "Order")
|
||||
.WithMany()
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Employee");
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Contract", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Employee", "Employee")
|
||||
|
||||
@@ -16,7 +16,9 @@ public class OmsorgCoreDbContext : DbContext
|
||||
public DbSet<EmployeeFacilityDistance> EmployeeFacilityDistances => Set<EmployeeFacilityDistance>();
|
||||
public DbSet<Contract> Contracts => Set<Contract>();
|
||||
public DbSet<Order> Orders => Set<Order>();
|
||||
public DbSet<Assignment> Assignments => Set<Assignment>();
|
||||
public DbSet<Absence> Absences => Set<Absence>();
|
||||
public DbSet<AssignmentValidationSettings> AssignmentValidationSettings => Set<AssignmentValidationSettings>();
|
||||
public DbSet<ValueList> ValueLists => Set<ValueList>();
|
||||
public DbSet<ValueListItem> ValueListItems => Set<ValueListItem>();
|
||||
public DbSet<ValueListItemTransition> ValueListItemTransitions => Set<ValueListItemTransition>();
|
||||
|
||||
@@ -58,6 +58,25 @@ public class AbsenceRepository : IAbsenceRepository
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<bool> HasOverlappingAbsenceAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
IReadOnlyCollection<string> blockingStatusValues,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (blockingStatusValues.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _db.Absences
|
||||
.Where(a => a.EmployeeId == employeeId && !a.IsDeleted)
|
||||
.Where(a => blockingStatusValues.Contains(a.Status))
|
||||
.Where(a => a.StartDate <= endDate && a.EndDate >= startDate)
|
||||
.AnyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddAsync(Absence absence, CancellationToken cancellationToken = default)
|
||||
=> await _db.Absences.AddAsync(absence, cancellationToken);
|
||||
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Infrastructure.Persistence;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Repositories;
|
||||
|
||||
public class AssignmentRepository : IAssignmentRepository
|
||||
{
|
||||
private readonly OmsorgCoreDbContext _db;
|
||||
|
||||
public AssignmentRepository(OmsorgCoreDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public Task<Assignment?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> _db.Assignments.Include(a => a.Employee).FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<Assignment>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
=> await _db.Assignments.AsNoTracking().Include(a => a.Employee).ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<(IReadOnlyList<Assignment> Items, int TotalCount)> GetPagedAsync(
|
||||
Guid? orderId = null,
|
||||
Guid? employeeId = null,
|
||||
DateOnly? fromDate = null,
|
||||
DateOnly? toDate = null,
|
||||
int page = 1,
|
||||
int pageSize = 20,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments.AsNoTracking().Include(a => a.Employee).Where(a => !a.IsDeleted);
|
||||
|
||||
if (orderId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.OrderId == orderId.Value);
|
||||
}
|
||||
|
||||
if (employeeId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.EmployeeId == employeeId.Value);
|
||||
}
|
||||
|
||||
if (fromDate.HasValue && toDate.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.StartDate <= toDate.Value && a.EndDate >= fromDate.Value);
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var items = await query
|
||||
.OrderBy(a => a.StartDate)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public async Task<bool> HasOverlapAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments
|
||||
.Where(a => a.EmployeeId == employeeId && !a.IsDeleted)
|
||||
.Where(a => a.StartDate <= endDate && a.EndDate >= startDate);
|
||||
|
||||
if (excludeAssignmentId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.Id != excludeAssignmentId.Value);
|
||||
}
|
||||
|
||||
return await query.AnyAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<int> GetAssignedDayCountInWeekAsync(
|
||||
Guid employeeId,
|
||||
DateOnly weekStart,
|
||||
DateOnly weekEnd,
|
||||
Guid? excludeAssignmentId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments
|
||||
.Where(a => a.EmployeeId == employeeId && !a.IsDeleted)
|
||||
.Where(a => a.StartDate <= weekEnd && a.EndDate >= weekStart);
|
||||
|
||||
if (excludeAssignmentId.HasValue)
|
||||
{
|
||||
query = query.Where(a => a.Id != excludeAssignmentId.Value);
|
||||
}
|
||||
|
||||
var overlapping = await query
|
||||
.Select(a => new { a.StartDate, a.EndDate })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var days = 0;
|
||||
foreach (var a in overlapping)
|
||||
{
|
||||
var rangeStart = a.StartDate > weekStart ? a.StartDate : weekStart;
|
||||
var rangeEnd = a.EndDate < weekEnd ? a.EndDate : weekEnd;
|
||||
days += rangeEnd.DayNumber - rangeStart.DayNumber + 1;
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
public async Task<int> CountForOrderAsync(Guid orderId, CancellationToken cancellationToken = default)
|
||||
=> await _db.Assignments.CountAsync(a => a.OrderId == orderId && !a.IsDeleted, cancellationToken);
|
||||
|
||||
public async Task AddAsync(Assignment assignment, CancellationToken cancellationToken = default)
|
||||
=> await _db.Assignments.AddAsync(assignment, cancellationToken);
|
||||
|
||||
public Task UpdateAsync(Assignment assignment, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.Assignments.Update(assignment);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assignment = await _db.Assignments.FirstOrDefaultAsync(a => a.Id == id, cancellationToken);
|
||||
if (assignment is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
assignment.IsDeleted = true;
|
||||
assignment.DeletedAt = DateTime.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Assignment>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.Assignments.IgnoreQueryFilters().AsNoTracking().Include(a => a.Employee).Where(a => a.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
query = query.Where(a =>
|
||||
EF.Functions.ILike(a.Employee.FirstName, $"%{search}%") ||
|
||||
EF.Functions.ILike(a.Employee.LastName, $"%{search}%"));
|
||||
}
|
||||
|
||||
return await query.OrderByDescending(a => a.DeletedAt).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var assignment = await _db.Assignments.IgnoreQueryFilters().FirstOrDefaultAsync(a => a.Id == id && a.IsDeleted, cancellationToken);
|
||||
if (assignment is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
assignment.IsDeleted = false;
|
||||
assignment.DeletedAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Infrastructure.Persistence;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Repositories;
|
||||
|
||||
public class AssignmentValidationSettingsRepository : IAssignmentValidationSettingsRepository
|
||||
{
|
||||
private readonly OmsorgCoreDbContext _db;
|
||||
|
||||
public AssignmentValidationSettingsRepository(OmsorgCoreDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<AssignmentValidationSettings> GetOrCreateAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var settings = await _db.AssignmentValidationSettings.FirstOrDefaultAsync(cancellationToken);
|
||||
if (settings is not null)
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
settings = new AssignmentValidationSettings();
|
||||
await _db.AssignmentValidationSettings.AddAsync(settings, cancellationToken);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return settings;
|
||||
}
|
||||
|
||||
public Task UpdateAsync(AssignmentValidationSettings settings, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.AssignmentValidationSettings.Update(settings);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -63,6 +63,16 @@ public class ContractRepository : IContractRepository
|
||||
return (items, totalCount);
|
||||
}
|
||||
|
||||
public Task<Contract?> GetActiveForEmployeeCoveringRangeAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> _db.Contracts
|
||||
.Where(c => !c.IsDeleted && c.EmployeeId == employeeId && c.Status != "Entwurf")
|
||||
.Where(c => c.StartDate <= startDate && (c.EndDate == null || c.EndDate >= endDate))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public async Task AddAsync(Contract contract, CancellationToken cancellationToken = default)
|
||||
=> await _db.Contracts.AddAsync(contract, cancellationToken);
|
||||
|
||||
|
||||
@@ -492,6 +492,15 @@ public class FakeContractRepository : IContractRepository
|
||||
return Task.FromResult<(IReadOnlyList<Contract> Items, int TotalCount)>((items, totalCount));
|
||||
}
|
||||
|
||||
public Task<Contract?> GetActiveForEmployeeCoveringRangeAsync(
|
||||
Guid employeeId,
|
||||
DateOnly startDate,
|
||||
DateOnly endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> Task.FromResult(_contracts.FirstOrDefault(c =>
|
||||
!c.IsDeleted && c.EmployeeId == employeeId && c.Status != "Entwurf" &&
|
||||
c.StartDate <= startDate && (c.EndDate == null || c.EndDate >= endDate)));
|
||||
|
||||
public Task AddAsync(Contract contract, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_contracts.Add(contract);
|
||||
|
||||
Reference in New Issue
Block a user