Add kilometer-based Fahrtkosten billing, self-service distance entry, role deletion
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 16s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 5s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 19s
Docker-Images bauen und veröffentlichen / build (, omsorgCore/Dockerfile, omsorgcore) (push) Successful in 16s
Docker-Images bauen und veröffentlichen / build (, omsorgWeb/Dockerfile, omsorgweb) (push) Successful in 5s
Docker-Images bauen und veröffentlichen / build (, omsorgapp/Dockerfile, omsorgapp) (push) Successful in 19s
- Facility: TravelCostMode (Pauschale/ProKilometer) + TravelCostPerKm, alongside the existing flat rate; fixes FacilityService.UpdateAsync silently dropping all Konditionen fields on update. - New EmployeeFacilityDistance (Mitarbeiter x Einrichtung -> km) with full office-side CRUD in omsorgapp, plus a self-service endpoint/UI so field staff can maintain their own commute distance via OMSORG Connect (new ModuleType.EmployeeFacilityDistances, Own-scope, no Facilities access needed). - Roles can now be deleted (blocked with a 409 while still assigned to a user). - Fix employeesApi.js missing the Date-object conversion for dateOfBirth/entryDate/ exitDate that crashed employee creation whenever a date was filled in; add a clear/remove control for those date fields. - Requirements: flesh out FR-REC-3 with a staged Akquise cadence, add FR-REC-5/6 for CRM feature scope and success metrics. - Regenerate api-client-ts and api-client-php for all of the above. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
09dce2ab98
commit
ffa2c4a9e7
@@ -0,0 +1,5 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record CreateEmployeeFacilityDistanceRequest(
|
||||
Guid EmployeeId,
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record CreateMyFacilityDistanceRequest(
|
||||
Guid FacilityId,
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record EmployeeFacilityDistanceResponse(
|
||||
Guid Id,
|
||||
Guid FacilityId,
|
||||
Guid EmployeeId,
|
||||
string EmployeeFirstName,
|
||||
string EmployeeLastName,
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Minimale Einrichtungsauswahl für Selbstbedienungs-Formulare ohne Facilities-Recht (z. B.
|
||||
/// MyFacilityDistancesController) — bewusst nur Id/Name, keine Konditionen/CRM-Daten.
|
||||
/// </summary>
|
||||
public record FacilityOptionResponse(
|
||||
Guid Id,
|
||||
string Name);
|
||||
@@ -21,6 +21,8 @@ public record FacilityResponse(
|
||||
decimal? SundaySurchargePercent,
|
||||
decimal? HolidaySurchargePercent,
|
||||
decimal? TravelCostRate,
|
||||
string TravelCostMode,
|
||||
decimal? TravelCostPerKm,
|
||||
decimal? MinimumHours,
|
||||
string? BreakPolicy,
|
||||
string? BillingInterval,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record MyFacilityDistanceResponse(
|
||||
Guid Id,
|
||||
Guid FacilityId,
|
||||
string FacilityName,
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record TrashEmployeeFacilityDistanceResponse(
|
||||
Guid Id,
|
||||
Guid FacilityId,
|
||||
Guid EmployeeId,
|
||||
decimal DistanceKm,
|
||||
DateTime? DeletedAt);
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateEmployeeFacilityDistanceRequest(
|
||||
decimal DistanceKm);
|
||||
@@ -20,6 +20,8 @@ public record UpdateFacilityRequest(
|
||||
decimal? SundaySurchargePercent,
|
||||
decimal? HolidaySurchargePercent,
|
||||
decimal? TravelCostRate,
|
||||
string TravelCostMode,
|
||||
decimal? TravelCostPerKm,
|
||||
decimal? MinimumHours,
|
||||
string? BreakPolicy,
|
||||
string? BillingInterval,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace OmsorgCore.Api.Contracts;
|
||||
|
||||
public record UpdateMyFacilityDistanceRequest(
|
||||
decimal DistanceKm);
|
||||
@@ -0,0 +1,137 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Mitarbeiter-Einrichtung-Entfernungen (km) sind eine 1:n-Unterressource von Facility (FR-EIN-4,
|
||||
/// Grundlage für Facility.TravelCostMode == "ProKilometer") — kein eigenständiges Core-Objekt, daher
|
||||
/// unter /api/facilities/{facilityId}/employee-distances und mit den gleichen Facilities-Rechten
|
||||
/// gegated statt einem eigenen ModuleType, analog zu FacilityQualificationRate.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/facilities/{facilityId:guid}/employee-distances")]
|
||||
public class EmployeeFacilityDistancesController : ControllerBase
|
||||
{
|
||||
private readonly IFacilityService _facilityService;
|
||||
private readonly IEmployeeService _employeeService;
|
||||
private readonly IEmployeeFacilityDistanceService _employeeFacilityDistanceService;
|
||||
|
||||
public EmployeeFacilityDistancesController(
|
||||
IFacilityService facilityService,
|
||||
IEmployeeService employeeService,
|
||||
IEmployeeFacilityDistanceService employeeFacilityDistanceService)
|
||||
{
|
||||
_facilityService = facilityService;
|
||||
_employeeService = employeeService;
|
||||
_employeeFacilityDistanceService = employeeFacilityDistanceService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.View)]
|
||||
public async Task<ActionResult<IReadOnlyList<EmployeeFacilityDistanceResponse>>> GetAll(Guid facilityId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _facilityService.GetByIdAsync(facilityId, cancellationToken) is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var distances = await _employeeFacilityDistanceService.GetByFacilityIdAsync(facilityId, cancellationToken);
|
||||
return Ok(distances.Select(d => ToResponse(d, d.Employee)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Create)]
|
||||
public async Task<ActionResult<EmployeeFacilityDistanceResponse>> Create(Guid facilityId, CreateEmployeeFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (await _facilityService.GetByIdAsync(facilityId, cancellationToken) is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var employee = await _employeeService.GetByIdAsync(request.EmployeeId, cancellationToken);
|
||||
if (employee is null)
|
||||
{
|
||||
return BadRequest("EmployeeId verweist auf keinen existierenden Mitarbeiter.");
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
if (await _employeeFacilityDistanceService.GetByFacilityAndEmployeeAsync(facilityId, request.EmployeeId, cancellationToken) is not null)
|
||||
{
|
||||
return BadRequest("Für diesen Mitarbeiter existiert bereits eine Entfernung zu dieser Einrichtung.");
|
||||
}
|
||||
|
||||
var distance = new EmployeeFacilityDistance
|
||||
{
|
||||
FacilityId = facilityId,
|
||||
EmployeeId = request.EmployeeId,
|
||||
DistanceKm = request.DistanceKm
|
||||
};
|
||||
|
||||
var created = await _employeeFacilityDistanceService.CreateAsync(distance, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetAll), new { facilityId }, ToResponse(created, employee));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<EmployeeFacilityDistanceResponse>> Update(Guid facilityId, Guid id, UpdateEmployeeFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _employeeFacilityDistanceService.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null || existing.FacilityId != facilityId)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
var updates = new EmployeeFacilityDistance
|
||||
{
|
||||
DistanceKm = request.DistanceKm
|
||||
};
|
||||
|
||||
var updated = await _employeeFacilityDistanceService.UpdateAsync(id, updates, cancellationToken);
|
||||
if (updated is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var employee = await _employeeService.GetByIdAsync(updated.EmployeeId, cancellationToken);
|
||||
return Ok(ToResponse(updated, employee!));
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Delete)]
|
||||
public async Task<IActionResult> Delete(Guid facilityId, Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var existing = await _employeeFacilityDistanceService.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null || existing.FacilityId != facilityId)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var deleted = await _employeeFacilityDistanceService.DeleteAsync(id, cancellationToken);
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
private static EmployeeFacilityDistanceResponse ToResponse(EmployeeFacilityDistance distance, Employee employee)
|
||||
=> new(
|
||||
distance.Id,
|
||||
distance.FacilityId,
|
||||
distance.EmployeeId,
|
||||
employee.FirstName,
|
||||
employee.LastName,
|
||||
distance.DistanceKm);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public class FacilitiesController : ControllerBase
|
||||
private const string FacilityTypeListKey = "FacilityType";
|
||||
private const string FollowUpPeriodsListKey = "FollowUpPeriods";
|
||||
private const string BillingIntervalListKey = "BillingInterval";
|
||||
private static readonly string[] TravelCostModes = { "Pauschale", "ProKilometer" };
|
||||
|
||||
private readonly IFacilityService _facilityService;
|
||||
private readonly IValueListRepository _valueListRepository;
|
||||
@@ -222,6 +223,7 @@ public class FacilitiesController : ControllerBase
|
||||
|
||||
if (request.BillingRate is < 0
|
||||
|| request.TravelCostRate is < 0
|
||||
|| request.TravelCostPerKm is < 0
|
||||
|| request.MinimumHours is < 0
|
||||
|| request.NightSurchargePercent is < 0
|
||||
|| request.SaturdaySurchargePercent is < 0
|
||||
@@ -232,6 +234,11 @@ public class FacilitiesController : ControllerBase
|
||||
return BadRequest("Konditionswerte dürfen nicht negativ sein.");
|
||||
}
|
||||
|
||||
if (!TravelCostModes.Contains(request.TravelCostMode))
|
||||
{
|
||||
return BadRequest($"TravelCostMode muss einer der folgenden Werte sein: {string.Join(", ", TravelCostModes)}.");
|
||||
}
|
||||
|
||||
var updates = new Facility
|
||||
{
|
||||
Name = request.Name,
|
||||
@@ -243,6 +250,8 @@ public class FacilitiesController : ControllerBase
|
||||
SundaySurchargePercent = request.SundaySurchargePercent,
|
||||
HolidaySurchargePercent = request.HolidaySurchargePercent,
|
||||
TravelCostRate = request.TravelCostRate,
|
||||
TravelCostMode = request.TravelCostMode,
|
||||
TravelCostPerKm = request.TravelCostPerKm,
|
||||
MinimumHours = request.MinimumHours,
|
||||
BreakPolicy = request.BreakPolicy,
|
||||
BillingInterval = request.BillingInterval,
|
||||
@@ -319,6 +328,8 @@ public class FacilitiesController : ControllerBase
|
||||
facility.SundaySurchargePercent,
|
||||
facility.HolidaySurchargePercent,
|
||||
facility.TravelCostRate,
|
||||
facility.TravelCostMode,
|
||||
facility.TravelCostPerKm,
|
||||
facility.MinimumHours,
|
||||
facility.BreakPolicy,
|
||||
facility.BillingInterval,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using OmsorgCore.Api.Contracts;
|
||||
using OmsorgCore.Api.Security;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Application.Services;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Domain.Enums;
|
||||
|
||||
namespace OmsorgCore.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Selbstbedienungs-Endpoint für Außendienst-Mitarbeiter (OMSORG Connect,
|
||||
/// `omsorgWeb/mitarbeiter-app`): eigene Fahrtstrecke (km) je Einrichtung pflegen, Grundlage für die
|
||||
/// kilometerbasierte Fahrtkostenabrechnung (Facility.TravelCostMode == "ProKilometer", FR-EIN-4).
|
||||
/// Bewusst ein eigener ModuleType (EmployeeFacilityDistances) statt Facilities-Rechten — der
|
||||
/// Außendienst darf hier ausschließlich seine eigenen Datensätze sehen/anlegen/bearbeiten, nicht die
|
||||
/// Konditionen/CRM-Daten einer Einrichtung. EmployeeId kommt IMMER serverseitig aus dem JWT-Claim,
|
||||
/// nie vom Client (analog AbsencesController/TimeEntriesController).
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("api/me/facility-distances")]
|
||||
public class MyFacilityDistancesController : ControllerBase
|
||||
{
|
||||
private readonly IEmployeeFacilityDistanceService _employeeFacilityDistanceService;
|
||||
private readonly IFacilityService _facilityService;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public MyFacilityDistancesController(
|
||||
IEmployeeFacilityDistanceService employeeFacilityDistanceService,
|
||||
IFacilityService facilityService,
|
||||
ICurrentUserService currentUserService)
|
||||
{
|
||||
_employeeFacilityDistanceService = employeeFacilityDistanceService;
|
||||
_facilityService = facilityService;
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.View)]
|
||||
public async Task<ActionResult<IReadOnlyList<MyFacilityDistanceResponse>>> GetAll(CancellationToken cancellationToken)
|
||||
{
|
||||
var employeeId = RequireOwnEmployeeId();
|
||||
if (employeeId is null)
|
||||
{
|
||||
return BadRequest("Dieser Benutzer ist mit keinem Mitarbeiter verknüpft.");
|
||||
}
|
||||
|
||||
var facilities = (await _facilityService.GetAllAsync(cancellationToken)).ToDictionary(f => f.Id);
|
||||
var all = new List<MyFacilityDistanceResponse>();
|
||||
|
||||
foreach (var facility in facilities.Values)
|
||||
{
|
||||
var distance = await _employeeFacilityDistanceService.GetByFacilityAndEmployeeAsync(facility.Id, employeeId.Value, cancellationToken);
|
||||
if (distance is not null)
|
||||
{
|
||||
all.Add(new MyFacilityDistanceResponse(distance.Id, facility.Id, facility.Name, distance.DistanceKm));
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(all);
|
||||
}
|
||||
|
||||
[HttpGet("facilities")]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.View)]
|
||||
public async Task<ActionResult<IReadOnlyList<FacilityOptionResponse>>> GetFacilityOptions(CancellationToken cancellationToken)
|
||||
{
|
||||
var facilities = await _facilityService.GetAllAsync(cancellationToken);
|
||||
return Ok(facilities.OrderBy(f => f.Name).Select(f => new FacilityOptionResponse(f.Id, f.Name)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.Create)]
|
||||
public async Task<ActionResult<MyFacilityDistanceResponse>> Create(CreateMyFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var employeeId = RequireOwnEmployeeId();
|
||||
if (employeeId is null)
|
||||
{
|
||||
return BadRequest("Dieser Benutzer ist mit keinem Mitarbeiter verknüpft.");
|
||||
}
|
||||
|
||||
var facility = await _facilityService.GetByIdAsync(request.FacilityId, cancellationToken);
|
||||
if (facility is null)
|
||||
{
|
||||
return BadRequest("FacilityId verweist auf keine existierende Einrichtung.");
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
if (await _employeeFacilityDistanceService.GetByFacilityAndEmployeeAsync(request.FacilityId, employeeId.Value, cancellationToken) is not null)
|
||||
{
|
||||
return BadRequest("Für diese Einrichtung existiert bereits eine eigene Entfernung.");
|
||||
}
|
||||
|
||||
var distance = new EmployeeFacilityDistance
|
||||
{
|
||||
FacilityId = request.FacilityId,
|
||||
EmployeeId = employeeId.Value,
|
||||
DistanceKm = request.DistanceKm
|
||||
};
|
||||
|
||||
var created = await _employeeFacilityDistanceService.CreateAsync(distance, cancellationToken);
|
||||
return CreatedAtAction(nameof(GetAll), new MyFacilityDistanceResponse(created.Id, facility.Id, facility.Name, created.DistanceKm));
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[RequirePermission(ModuleType.EmployeeFacilityDistances, PermissionAction.Edit)]
|
||||
public async Task<ActionResult<MyFacilityDistanceResponse>> Update(Guid id, UpdateMyFacilityDistanceRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
var employeeId = RequireOwnEmployeeId();
|
||||
if (employeeId is null)
|
||||
{
|
||||
return BadRequest("Dieser Benutzer ist mit keinem Mitarbeiter verknüpft.");
|
||||
}
|
||||
|
||||
var existing = await _employeeFacilityDistanceService.GetByIdAsync(id, cancellationToken);
|
||||
if (existing is null || existing.EmployeeId != employeeId.Value)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (request.DistanceKm < 0)
|
||||
{
|
||||
return BadRequest("DistanceKm darf nicht negativ sein.");
|
||||
}
|
||||
|
||||
var updated = await _employeeFacilityDistanceService.UpdateAsync(id, new EmployeeFacilityDistance { DistanceKm = request.DistanceKm }, cancellationToken);
|
||||
if (updated is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var facility = await _facilityService.GetByIdAsync(updated.FacilityId, cancellationToken);
|
||||
return Ok(new MyFacilityDistanceResponse(updated.Id, updated.FacilityId, facility?.Name ?? "", updated.DistanceKm));
|
||||
}
|
||||
|
||||
private Guid? RequireOwnEmployeeId()
|
||||
=> _currentUserService.EmployeeId is { } id && id != Guid.Empty ? id : null;
|
||||
}
|
||||
@@ -84,4 +84,22 @@ public class RolesController : ControllerBase
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[RequirePermission(ModuleType.UserManagement, PermissionAction.Delete)]
|
||||
public async Task<IActionResult> Delete(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await _roleService.DeleteAsync(id, cancellationToken);
|
||||
if (!result.Success)
|
||||
{
|
||||
return result.FailureReason switch
|
||||
{
|
||||
DeleteRoleFailureReason.RoleNotFound => NotFound("Rolle nicht gefunden."),
|
||||
DeleteRoleFailureReason.InUse => Conflict($"Diese Rolle ist noch {result.AssignedUserCount} Benutzer(n) zugewiesen und kann nicht gelöscht werden."),
|
||||
_ => BadRequest()
|
||||
};
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public class TrashController : ControllerBase
|
||||
private readonly IOrderService _orderService;
|
||||
private readonly IFacilityContactService _facilityContactService;
|
||||
private readonly IFacilityQualificationRateService _facilityQualificationRateService;
|
||||
private readonly IEmployeeFacilityDistanceService _employeeFacilityDistanceService;
|
||||
private readonly IAbsenceService _absenceService;
|
||||
private readonly ITimeEntryService _timeEntryService;
|
||||
|
||||
@@ -36,6 +37,7 @@ public class TrashController : ControllerBase
|
||||
IOrderService orderService,
|
||||
IFacilityContactService facilityContactService,
|
||||
IFacilityQualificationRateService facilityQualificationRateService,
|
||||
IEmployeeFacilityDistanceService employeeFacilityDistanceService,
|
||||
IAbsenceService absenceService,
|
||||
ITimeEntryService timeEntryService)
|
||||
{
|
||||
@@ -45,6 +47,7 @@ public class TrashController : ControllerBase
|
||||
_orderService = orderService;
|
||||
_facilityContactService = facilityContactService;
|
||||
_facilityQualificationRateService = facilityQualificationRateService;
|
||||
_employeeFacilityDistanceService = employeeFacilityDistanceService;
|
||||
_absenceService = absenceService;
|
||||
_timeEntryService = timeEntryService;
|
||||
}
|
||||
@@ -145,6 +148,22 @@ public class TrashController : ControllerBase
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("employee-facility-distances")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Recover)]
|
||||
public async Task<ActionResult<IReadOnlyList<TrashEmployeeFacilityDistanceResponse>>> GetDeletedEmployeeFacilityDistances([FromQuery] string? search, CancellationToken cancellationToken)
|
||||
{
|
||||
var distances = await _employeeFacilityDistanceService.GetDeletedAsync(search, cancellationToken);
|
||||
return Ok(distances.Select(d => new TrashEmployeeFacilityDistanceResponse(d.Id, d.FacilityId, d.EmployeeId, d.DistanceKm, d.DeletedAt)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("employee-facility-distances/{id:guid}/restore")]
|
||||
[RequirePermission(ModuleType.Facilities, PermissionAction.Recover)]
|
||||
public async Task<IActionResult> RestoreEmployeeFacilityDistance(Guid id, CancellationToken cancellationToken)
|
||||
{
|
||||
var restored = await _employeeFacilityDistanceService.RestoreAsync(id, cancellationToken);
|
||||
return restored ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("absences")]
|
||||
[RequirePermission(ModuleType.Absences, PermissionAction.Recover)]
|
||||
public async Task<ActionResult<IReadOnlyList<TrashAbsenceResponse>>> GetDeletedAbsences([FromQuery] string? search, CancellationToken cancellationToken)
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Abstractions;
|
||||
|
||||
public interface IEmployeeFacilityDistanceRepository
|
||||
{
|
||||
Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default);
|
||||
Task UpdateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default);
|
||||
Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default);
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -18,5 +18,6 @@ public interface IRoleRepository
|
||||
Task AddPermissionRangeAsync(IEnumerable<RolePermission> permissions, CancellationToken cancellationToken = default);
|
||||
Task<bool> ExistsByNameAsync(string name, CancellationToken cancellationToken = default);
|
||||
Task AddAsync(Role role, CancellationToken cancellationToken = default);
|
||||
Task RemoveAsync(Role role, CancellationToken cancellationToken = default);
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IFacilityService, FacilityService>();
|
||||
services.AddScoped<IFacilityContactService, FacilityContactService>();
|
||||
services.AddScoped<IFacilityQualificationRateService, FacilityQualificationRateService>();
|
||||
services.AddScoped<IEmployeeFacilityDistanceService, EmployeeFacilityDistanceService>();
|
||||
services.AddScoped<IContractService, ContractService>();
|
||||
services.AddScoped<IOrderService, OrderService>();
|
||||
services.AddScoped<IAbsenceService, AbsenceService>();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public enum DeleteRoleFailureReason
|
||||
{
|
||||
RoleNotFound,
|
||||
InUse
|
||||
}
|
||||
|
||||
public class DeleteRoleResult
|
||||
{
|
||||
public bool Success { get; init; }
|
||||
public DeleteRoleFailureReason? FailureReason { get; init; }
|
||||
|
||||
/// <summary>Nur gesetzt bei <see cref="DeleteRoleFailureReason.InUse"/> - Anzahl der Benutzer, die dieser Rolle noch zugewiesen sind.</summary>
|
||||
public int AssignedUserCount { get; init; }
|
||||
|
||||
public static DeleteRoleResult Fail(DeleteRoleFailureReason reason, int assignedUserCount = 0) => new()
|
||||
{
|
||||
Success = false,
|
||||
FailureReason = reason,
|
||||
AssignedUserCount = assignedUserCount
|
||||
};
|
||||
|
||||
public static DeleteRoleResult Ok() => new()
|
||||
{
|
||||
Success = true
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public class EmployeeFacilityDistanceService : IEmployeeFacilityDistanceService
|
||||
{
|
||||
private readonly IEmployeeFacilityDistanceRepository _employeeFacilityDistanceRepository;
|
||||
|
||||
public EmployeeFacilityDistanceService(IEmployeeFacilityDistanceRepository employeeFacilityDistanceRepository)
|
||||
{
|
||||
_employeeFacilityDistanceRepository = employeeFacilityDistanceRepository;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetByFacilityIdAsync(facilityId, cancellationToken);
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetByIdAsync(id, cancellationToken);
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetByFacilityAndEmployeeAsync(facilityId, employeeId, cancellationToken);
|
||||
|
||||
public async Task<EmployeeFacilityDistance> CreateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _employeeFacilityDistanceRepository.AddAsync(distance, cancellationToken);
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
return distance;
|
||||
}
|
||||
|
||||
public async Task<EmployeeFacilityDistance?> UpdateAsync(Guid id, EmployeeFacilityDistance updates, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = await _employeeFacilityDistanceRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (distance is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
distance.DistanceKm = updates.DistanceKm;
|
||||
distance.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _employeeFacilityDistanceRepository.UpdateAsync(distance, cancellationToken);
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
return distance;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var deleted = await _employeeFacilityDistanceRepository.SoftDeleteAsync(id, cancellationToken);
|
||||
if (deleted)
|
||||
{
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default)
|
||||
=> _employeeFacilityDistanceRepository.GetDeletedAsync(search, cancellationToken);
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var restored = await _employeeFacilityDistanceRepository.RestoreAsync(id, cancellationToken);
|
||||
if (restored)
|
||||
{
|
||||
await _employeeFacilityDistanceRepository.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return restored;
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,19 @@ public class FacilityService : IFacilityService
|
||||
facility.BillingCountry = updates.BillingCountry;
|
||||
facility.CrmStatus = updates.CrmStatus;
|
||||
facility.FollowUpDueDate = updates.FollowUpDueDate;
|
||||
facility.BillingRate = updates.BillingRate;
|
||||
facility.NightSurchargePercent = updates.NightSurchargePercent;
|
||||
facility.SaturdaySurchargePercent = updates.SaturdaySurchargePercent;
|
||||
facility.SundaySurchargePercent = updates.SundaySurchargePercent;
|
||||
facility.HolidaySurchargePercent = updates.HolidaySurchargePercent;
|
||||
facility.TravelCostRate = updates.TravelCostRate;
|
||||
facility.TravelCostMode = updates.TravelCostMode;
|
||||
facility.TravelCostPerKm = updates.TravelCostPerKm;
|
||||
facility.MinimumHours = updates.MinimumHours;
|
||||
facility.BreakPolicy = updates.BreakPolicy;
|
||||
facility.BillingInterval = updates.BillingInterval;
|
||||
facility.PaymentTermDays = updates.PaymentTermDays;
|
||||
facility.IndividualAgreements = updates.IndividualAgreements;
|
||||
facility.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _facilityRepository.UpdateAsync(facility, cancellationToken);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Application.Services;
|
||||
|
||||
public interface IEmployeeFacilityDistanceService
|
||||
{
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance> CreateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default);
|
||||
Task<EmployeeFacilityDistance?> UpdateAsync(Guid id, EmployeeFacilityDistance updates, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default);
|
||||
Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -16,4 +16,7 @@ public interface IRoleService
|
||||
Guid roleId,
|
||||
IReadOnlyList<(ModuleType Module, PermissionAction Action, PermissionScope Scope)> permissions,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>Löschschutz: schlägt fehl, solange mindestens ein User dieser Rolle zugewiesen ist (analog ValueListService.DeleteItemAsync).</summary>
|
||||
Task<DeleteRoleResult> DeleteAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ public class RoleService : IRoleService
|
||||
{
|
||||
private readonly IRoleRepository _roleRepository;
|
||||
private readonly IPermissionService _permissionService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public RoleService(IRoleRepository roleRepository, IPermissionService permissionService)
|
||||
public RoleService(IRoleRepository roleRepository, IPermissionService permissionService, IUserRepository userRepository)
|
||||
{
|
||||
_roleRepository = roleRepository;
|
||||
_permissionService = permissionService;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<Role>> GetAllAsync(CancellationToken cancellationToken = default)
|
||||
@@ -62,4 +64,23 @@ public class RoleService : IRoleService
|
||||
await _permissionService.InvalidateRolePermissionsAsync(roleId, cancellationToken);
|
||||
return UpdateRolePermissionsResult.Ok();
|
||||
}
|
||||
|
||||
public async Task<DeleteRoleResult> DeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var role = await _roleRepository.GetByIdAsync(id, cancellationToken);
|
||||
if (role is null)
|
||||
{
|
||||
return DeleteRoleResult.Fail(DeleteRoleFailureReason.RoleNotFound);
|
||||
}
|
||||
|
||||
var assignedUserIds = await _userRepository.GetUserIdsByRoleAsync(id, cancellationToken);
|
||||
if (assignedUserIds.Count > 0)
|
||||
{
|
||||
return DeleteRoleResult.Fail(DeleteRoleFailureReason.InUse, assignedUserIds.Count);
|
||||
}
|
||||
|
||||
await _roleRepository.RemoveAsync(role, cancellationToken);
|
||||
await _roleRepository.SaveChangesAsync(cancellationToken);
|
||||
return DeleteRoleResult.Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using OmsorgCore.Domain.Common;
|
||||
|
||||
namespace OmsorgCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Entfernung (km) zwischen einem Mitarbeiter und einer Einrichtung — Grundlage für die
|
||||
/// kilometerbasierte Fahrtkostenabrechnung (<see cref="Facility.TravelCostMode"/> == "ProKilometer",
|
||||
/// FR-EIN-4). Pro Mitarbeiter/Einrichtung individuell statt an Facility allein, da jeder Mitarbeiter
|
||||
/// von einem anderen Wohnort anfährt. 1:n-Unterressource von Facility, kein eigenständiges Core-Objekt
|
||||
/// — exakt nach dem Muster von <see cref="FacilityQualificationRate"/>.
|
||||
/// </summary>
|
||||
public class EmployeeFacilityDistance : AuditableEntity
|
||||
{
|
||||
public Guid FacilityId { get; set; }
|
||||
public Facility Facility { get; set; } = null!;
|
||||
|
||||
public Guid EmployeeId { get; set; }
|
||||
public Employee Employee { get; set; } = null!;
|
||||
|
||||
public decimal DistanceKm { get; set; }
|
||||
}
|
||||
@@ -33,7 +33,12 @@ public class Facility : AuditableEntity
|
||||
public decimal? SaturdaySurchargePercent { get; set; }
|
||||
public decimal? SundaySurchargePercent { get; set; }
|
||||
public decimal? HolidaySurchargePercent { get; set; }
|
||||
// TravelCostMode entscheidet, welches der beiden Felder die Rechnungserstellung (FR-RE-1,
|
||||
// sobald umgesetzt) verwendet: "Pauschale" -> TravelCostRate (EUR je Einsatz), "ProKilometer"
|
||||
// -> TravelCostPerKm (EUR/km) * EmployeeFacilityDistance.DistanceKm des jeweiligen Mitarbeiters.
|
||||
public string TravelCostMode { get; set; } = "Pauschale";
|
||||
public decimal? TravelCostRate { get; set; }
|
||||
public decimal? TravelCostPerKm { get; set; }
|
||||
public decimal? MinimumHours { get; set; }
|
||||
public string? BreakPolicy { get; set; }
|
||||
public string? BillingInterval { get; set; }
|
||||
|
||||
@@ -18,5 +18,6 @@ public enum ModuleType
|
||||
Documents,
|
||||
Users,
|
||||
Configuration,
|
||||
Absences
|
||||
Absences,
|
||||
EmployeeFacilityDistances
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public static class DependencyInjection
|
||||
services.AddScoped<IFacilityRepository, FacilityRepository>();
|
||||
services.AddScoped<IFacilityContactRepository, FacilityContactRepository>();
|
||||
services.AddScoped<IFacilityQualificationRateRepository, FacilityQualificationRateRepository>();
|
||||
services.AddScoped<IEmployeeFacilityDistanceRepository, EmployeeFacilityDistanceRepository>();
|
||||
services.AddScoped<IContractRepository, ContractRepository>();
|
||||
services.AddScoped<IOrderRepository, OrderRepository>();
|
||||
services.AddScoped<IAbsenceRepository, AbsenceRepository>();
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class EmployeeFacilityDistanceConfiguration : IEntityTypeConfiguration<EmployeeFacilityDistance>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<EmployeeFacilityDistance> builder)
|
||||
{
|
||||
builder.ToTable("employee_facility_distances");
|
||||
builder.HasKey(d => d.Id);
|
||||
builder.Property(d => d.DistanceKm).IsRequired().HasColumnType("decimal(10,2)");
|
||||
|
||||
builder.HasOne(d => d.Facility)
|
||||
.WithMany()
|
||||
.HasForeignKey(d => d.FacilityId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(d => d.Employee)
|
||||
.WithMany()
|
||||
.HasForeignKey(d => d.EmployeeId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Soft-gelöschte Distanzen sind für alle normalen Queries unsichtbar.
|
||||
builder.HasQueryFilter(d => !d.IsDeleted);
|
||||
}
|
||||
}
|
||||
+2
@@ -29,6 +29,8 @@ public class FacilityConfiguration : IEntityTypeConfiguration<Facility>
|
||||
builder.Property(f => f.SundaySurchargePercent).HasColumnType("decimal(5,2)");
|
||||
builder.Property(f => f.HolidaySurchargePercent).HasColumnType("decimal(5,2)");
|
||||
builder.Property(f => f.TravelCostRate).HasColumnType("decimal(10,2)");
|
||||
builder.Property(f => f.TravelCostMode).IsRequired().HasMaxLength(20);
|
||||
builder.Property(f => f.TravelCostPerKm).HasColumnType("decimal(10,2)");
|
||||
builder.Property(f => f.MinimumHours).HasColumnType("decimal(5,2)");
|
||||
builder.Property(f => f.BreakPolicy).HasMaxLength(1000);
|
||||
builder.Property(f => f.BillingInterval).HasMaxLength(50);
|
||||
|
||||
@@ -69,7 +69,11 @@ public static class DbSeeder
|
||||
/// User.EmployeeId. Ebenso Absences.{Create,View,Edit} mit PermissionScope.Own (FR-CON-1/FR-EM-3):
|
||||
/// der Außendienst stellt eigene Abwesenheits-/Urlaubs-/Krankmeldungsanträge über
|
||||
/// `omsorgWeb/mitarbeiter-app` gegen dieses Backend, Genehmigen/Ablehnen bleibt Büro-Rollen
|
||||
/// vorbehalten (kein Approve-Recht hier). "Geschäftsführung" bekommt AuditLog automatisch mit
|
||||
/// vorbehalten (kein Approve-Recht hier). Ebenso EmployeeFacilityDistances.{Create,View,Edit} mit
|
||||
/// PermissionScope.Own (FR-EIN-4, kilometerbasierte Fahrtkostenabrechnung, siehe
|
||||
/// MyFacilityDistancesController) - der Außendienst pflegt seine eigenen Fahrtstrecken je
|
||||
/// Einrichtung selbst über `omsorgWeb/mitarbeiter-app`, hat aber KEIN Facilities-Recht und sieht
|
||||
/// daher keine Konditionen/CRM-Daten einer Einrichtung, nur deren Namen zur Auswahl. "Geschäftsführung" bekommt AuditLog automatisch mit
|
||||
/// (iteriert alle ModuleType-Werte generisch,
|
||||
/// siehe unten) - alle anderen Rollen listen ihre Module explizit auf und bekommen AuditLog dadurch
|
||||
/// bewusst NICHT (Audit-Log ist per Default nur für Geschäftsführung sichtbar).
|
||||
@@ -104,7 +108,8 @@ public static class DbSeeder
|
||||
(ModuleType.Employees, new[] { PermissionAction.View }, PermissionScope.Own),
|
||||
(ModuleType.Contracts, 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.TimeEntries, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own),
|
||||
(ModuleType.EmployeeFacilityDistances, new[] { PermissionAction.Create, PermissionAction.View, PermissionAction.Edit }, PermissionScope.Own)
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
+1392
File diff suppressed because it is too large
Load Diff
+84
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddTravelCostModeAndEmployeeFacilityDistances : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TravelCostMode",
|
||||
table: "facilities",
|
||||
type: "character varying(20)",
|
||||
maxLength: 20,
|
||||
nullable: false,
|
||||
defaultValue: "Pauschale");
|
||||
|
||||
migrationBuilder.AddColumn<decimal>(
|
||||
name: "TravelCostPerKm",
|
||||
table: "facilities",
|
||||
type: "numeric(10,2)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "employee_facility_distances",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FacilityId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
EmployeeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
DistanceKm = table.Column<decimal>(type: "numeric(10,2)", nullable: false),
|
||||
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_employee_facility_distances", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_employee_facility_distances_employees_EmployeeId",
|
||||
column: x => x.EmployeeId,
|
||||
principalTable: "employees",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_employee_facility_distances_facilities_FacilityId",
|
||||
column: x => x.FacilityId,
|
||||
principalTable: "facilities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_employee_facility_distances_EmployeeId",
|
||||
table: "employee_facility_distances",
|
||||
column: "EmployeeId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_employee_facility_distances_FacilityId",
|
||||
table: "employee_facility_distances",
|
||||
column: "FacilityId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "employee_facility_distances");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TravelCostMode",
|
||||
table: "facilities");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TravelCostPerKm",
|
||||
table: "facilities");
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -361,6 +361,42 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("employees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.EmployeeFacilityDistance", 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<decimal>("DistanceKm")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<Guid>("EmployeeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("FacilityId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EmployeeId");
|
||||
|
||||
b.HasIndex("FacilityId");
|
||||
|
||||
b.ToTable("employee_facility_distances", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.Facility", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -458,6 +494,14 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.Property<decimal?>("SundaySurchargePercent")
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<string>("TravelCostMode")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<decimal?>("TravelCostPerKm")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<decimal?>("TravelCostRate")
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
@@ -1121,6 +1165,25 @@ namespace OmsorgCore.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("UploadedByUser");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.EmployeeFacilityDistance", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Employee", "Employee")
|
||||
.WithMany()
|
||||
.HasForeignKey("EmployeeId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Facility", "Facility")
|
||||
.WithMany()
|
||||
.HasForeignKey("FacilityId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Employee");
|
||||
|
||||
b.Navigation("Facility");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("OmsorgCore.Domain.Entities.FacilityContact", b =>
|
||||
{
|
||||
b.HasOne("OmsorgCore.Domain.Entities.Facility", "Facility")
|
||||
|
||||
@@ -13,6 +13,7 @@ public class OmsorgCoreDbContext : DbContext
|
||||
public DbSet<Facility> Facilities => Set<Facility>();
|
||||
public DbSet<FacilityContact> FacilityContacts => Set<FacilityContact>();
|
||||
public DbSet<FacilityQualificationRate> FacilityQualificationRates => Set<FacilityQualificationRate>();
|
||||
public DbSet<EmployeeFacilityDistance> EmployeeFacilityDistances => Set<EmployeeFacilityDistance>();
|
||||
public DbSet<Contract> Contracts => Set<Contract>();
|
||||
public DbSet<Order> Orders => Set<Order>();
|
||||
public DbSet<Absence> Absences => Set<Absence>();
|
||||
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OmsorgCore.Application.Abstractions;
|
||||
using OmsorgCore.Domain.Entities;
|
||||
using OmsorgCore.Infrastructure.Persistence;
|
||||
|
||||
namespace OmsorgCore.Infrastructure.Repositories;
|
||||
|
||||
public class EmployeeFacilityDistanceRepository : IEmployeeFacilityDistanceRepository
|
||||
{
|
||||
private readonly OmsorgCoreDbContext _db;
|
||||
|
||||
public EmployeeFacilityDistanceRepository(OmsorgCoreDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
=> _db.EmployeeFacilityDistances.FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<EmployeeFacilityDistance>> GetByFacilityIdAsync(Guid facilityId, CancellationToken cancellationToken = default)
|
||||
=> await _db.EmployeeFacilityDistances
|
||||
.AsNoTracking()
|
||||
.Include(d => d.Employee)
|
||||
.Where(d => d.FacilityId == facilityId)
|
||||
.OrderBy(d => d.Employee.LastName)
|
||||
.ThenBy(d => d.Employee.FirstName)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public Task<EmployeeFacilityDistance?> GetByFacilityAndEmployeeAsync(Guid facilityId, Guid employeeId, CancellationToken cancellationToken = default)
|
||||
=> _db.EmployeeFacilityDistances.FirstOrDefaultAsync(d => d.FacilityId == facilityId && d.EmployeeId == employeeId, cancellationToken);
|
||||
|
||||
public async Task AddAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
=> await _db.EmployeeFacilityDistances.AddAsync(distance, cancellationToken);
|
||||
|
||||
public Task UpdateAsync(EmployeeFacilityDistance distance, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.EmployeeFacilityDistances.Update(distance);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<bool> SoftDeleteAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = await _db.EmployeeFacilityDistances.FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
|
||||
if (distance is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
distance.IsDeleted = true;
|
||||
distance.DeletedAt = DateTime.UtcNow;
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<EmployeeFacilityDistance>> GetDeletedAsync(string? search, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = _db.EmployeeFacilityDistances.IgnoreQueryFilters().AsNoTracking().Include(d => d.Employee).Where(d => d.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
var pattern = $"%{search.Trim()}%";
|
||||
query = query.Where(d => EF.Functions.ILike(d.Employee.FirstName, pattern) || EF.Functions.ILike(d.Employee.LastName, pattern));
|
||||
}
|
||||
|
||||
return await query.OrderByDescending(d => d.DeletedAt).ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> RestoreAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var distance = await _db.EmployeeFacilityDistances.IgnoreQueryFilters().FirstOrDefaultAsync(d => d.Id == id && d.IsDeleted, cancellationToken);
|
||||
if (distance is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
distance.IsDeleted = false;
|
||||
distance.DeletedAt = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -34,6 +34,12 @@ public class RoleRepository : IRoleRepository
|
||||
public async Task AddAsync(Role role, CancellationToken cancellationToken = default)
|
||||
=> await _db.Roles.AddAsync(role, cancellationToken);
|
||||
|
||||
public Task RemoveAsync(Role role, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_db.Roles.Remove(role);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user