Abwesenheitsüberlappungsprüfung beim Klienten und Mitarbeiter eingebaut.

This commit is contained in:
2026-03-06 18:17:57 +01:00
parent dc2e8abe9c
commit b638a0f21e
22 changed files with 344 additions and 44 deletions

View File

@@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Web.Mvc;
using BeWo.Report.DefaultReports;
using BeWo.Report.DefaultReports;
using BeWo.View.Navigation.Filter;
using BeWoPlanerMobil.Models;
using BeWoPlanerMobil.Service;
@@ -15,6 +9,13 @@ using BS.Shared.DataContracts;
using BS.Shared.DataContracts.Compact;
using BS.Shared.Extensions;
using DevExpress.XtraReports.UI;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Web.Mvc;
namespace BeWoPlanerMobil.Controllers
{
@@ -1409,6 +1410,46 @@ namespace BeWoPlanerMobil.Controllers
{
return SaveCollapsibleStatusToModel(Model, isOpen, identifier);
}
[Authorize]
public string CheckForOverlappingAbsenceTimes(string startStr, string endStr, long oid)
{
if(false == DateTime.TryParse(startStr, out var start))
{
start = DateTime.MinValue;
}
if(false == DateTime.TryParse(endStr, out var end))
{
end = DateTime.MaxValue;
}
var messages = new List<string>();
var hasOverlappingAbsenceTimes = false;
foreach(var absenceTime in Model.SelectedCustomer.AbsenceTimes)
{
var alpha = absenceTime.Start ?? DateTime.MinValue;
var omega = absenceTime.End ?? DateTime.MaxValue;
if(start >= alpha && start <= omega ||
alpha >= start && alpha <= end ||
end >= alpha && end <= omega ||
omega >= start && omega <= end)
{
hasOverlappingAbsenceTimes = true;
break;
}
}
if(hasOverlappingAbsenceTimes)
{
messages.Add("Die Abwesenheit überschneidet sich mit mindestens einer bereits vorhandenen Abwesenheit.<br/><br/>Möchten Sie trotzdem fortfahren?");
}
return JsonConvert.SerializeObject(messages);
}
}
public enum CustomerReportType

View File

@@ -339,6 +339,19 @@ namespace BeWoPlanerMobil.Controllers
return PartialView("SchedulerPagePartial", Model);
}
[Authorize]
public string GetAppointmentType(long oid)
{
var appointment = Model.GetAppointmentByOid(oid);
var showMenuItem = false;
//var isTask = appointment.IsTask;
//var isAbsenceTime = appointment.
return JsonConvert.SerializeObject(showMenuItem);
}
[Authorize]
public ActionResult CheckEmployeeAvailability(string startString, string endString)
{

View File

@@ -1,12 +1,14 @@
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using BeWoPlanerMobil.Models;
using BeWoPlanerMobil.Service;
using BeWoPlanerMobil.Util;
using Bogus.DataSets;
using BS.Shared;
using BS.Shared.DataContracts;
using Newtonsoft.Json;
namespace BeWoPlanerMobil.Controllers
{
@@ -276,5 +278,59 @@ namespace BeWoPlanerMobil.Controllers
return PartialView("AbsenceTimePartial", Model);
}
[Authorize]
public string CheckForOverlappingAbsenceTimes(string startStr, string endStr, long oid)
{
var hasNoEnd = false;
if(false == DateTime.TryParse(startStr, out var start))
{
start = DateTime.MinValue;
}
if(false == DateTime.TryParse(endStr, out var end))
{
hasNoEnd = true;
end = DateTime.MaxValue;
}
var messages = new List<string>();
var hasOverlappingAbsenceTimes = false;
foreach(var absenceTime in Model.SelectedEmployee.AbsenceTimes)
{
var alpha = absenceTime.Start ?? DateTime.MinValue;
var omega = absenceTime.End ?? DateTime.MaxValue;
if(start >= alpha && start <= omega ||
alpha >= start && alpha <= end ||
end >= alpha && end <= omega ||
omega >= start && omega <= end)
{
hasOverlappingAbsenceTimes = true;
break;
}
}
if(hasOverlappingAbsenceTimes)
{
messages.Add($"Die Abwesenheit überschneidet sich mit mindestens einer bereits vorhandenen Abwesenheit.{(hasNoEnd ? "<br/><br/>" : "<br/>")}");
}
if(hasNoEnd)
{
messages.Add("Die Abwesenheit hat kein Enddatum.<br/>Beachten Sie, dass Abwesenheiten ohne Enddatum unbegrenzt weiterlaufen und es dadurch zu Fehlberechnungen im Stunden- und Urlaubskonto kommen kann.");
}
if(messages.Any())
{
messages.Add("<br/><br/>Möchten Sie trotzdem fortfahren?<br/>");
}
return JsonConvert.SerializeObject(messages);
}
}
}

View File

@@ -23,5 +23,8 @@ namespace BeWoPlanerMobil.Controllers
[Authorize]
ActionResult DeleteAbsenceTime(long absenceTimeOid);
[Authorize]
string CheckForOverlappingAbsenceTimes(string startStr, string endStr, long oid);
}
}

View File

@@ -4250,7 +4250,10 @@ namespace BeWoPlanerMobil.Controllers
Model.TransferringAppointmentOid = TempData[TempDataConstants.AppointmentOidKey] as long?;
var isSerientermin = TempData[TempDataConstants.AppointmentIsSerienterminKey] is true;
var recurrenceIndex = (int?) TempData[TempDataConstants.AppointmentRecurrenceIndexKey];
var isSerientermin = recurrenceIndex.HasValue;
//TempData[TempDataConstants.AppointmentIsSerienterminKey] is true;
if((false == Model.TransferringAppointmentOid.HasValue && false == isSerientermin) || false == Model.LoggedInEmployee.EmployeeOid.HasValue)
{
@@ -4437,7 +4440,9 @@ namespace BeWoPlanerMobil.Controllers
var pattern = StaticAppointmentFactory.CreateAppointment(AppointmentType.Pattern);
pattern.RecurrenceInfo.FromXml(recurrenceInfo);
var apt = pattern.CreateException(AppointmentType.ChangedOccurrence, appointment.RecurrenceIndex);
var apt = pattern.CreateException(AppointmentType.ChangedOccurrence, recurrenceIndex.Value);
var changedOccurrence = SchedulerAppointmentDC.RecurringExceptionFromDevExpressAppointment(apt, appointment);
var x = apt.RecurrenceIndex;
}

View File

@@ -1431,6 +1431,11 @@ namespace BeWoPlanerMobil.Controllers
TempData[TempDataConstants.AppointmentOidKey] = appointment?.SchedulerAppointmentOid;
if(appointment?.RecurrenceInfo != null)
{
TempData[TempDataConstants.AppointmentRecurrenceIndexKey] = appointment?.RecurrenceIndex;
}
TempData[TempDataConstants.AppointmentStartKey] = appointment?.StartDate;
TempData[TempDataConstants.AppointmentEndKey] = appointment?.EndDate;
TempData[TempDataConstants.AppointmentSubjectKey] = appointment?.Subject;

View File

@@ -16,6 +16,7 @@ namespace BeWoPlanerMobil.Models
public long? ObjectOid => SelectedCustomerOid;
public TableID ObjectTid => TableID.Customer;
public string HtmlPrefix => "customer";
public long? AbsenteeOid => SelectedCustomerOid;
public IAbsenceTime AbsenceTimeObject
{

View File

@@ -28,6 +28,7 @@ namespace BeWoPlanerMobil.Models
set => EmployeeSessionModel.SelectedAbsenceTimeObjectOid = value;
}
public long? AbsenteeOid => SelectedEmployeeOid;
public EmployeeModel()
{
SessionModel = new EmployeeSessionModel();

View File

@@ -13,5 +13,6 @@ namespace BeWoPlanerMobil.Models
IAbsenceTime AbsenceTimeObject { get; set; }
Dictionary<string, bool> Collapsibles2IsShown { get; set; }
List<SelectListItem> AbsenceReasonItems { get; }
long? AbsenteeOid { get; }
}
}

View File

@@ -552,4 +552,48 @@ function getTimeStamp(dateObject, showSeconds, showMilliseconds) {
} else {
return `${dd}.${MM}.${yyyy} ${HH}:${mm}`;
}
}
}
function getUnixTimeStamp(dateObject, showSeconds) {
const dd = String(dateObject.getDate()).padStart(2, "0");
const MM = String(dateObject.getMonth() + 1).padStart(2, "0");
const yyyy = dateObject.getFullYear();
const HH = String(dateObject.getHours()).padStart(2, "0");
const mm = String(dateObject.getMinutes()).padStart(2, "0");
const ss = String(dateObject.getSeconds()).padStart(2, "0");
const ms = String(dateObject.getMilliseconds()).padStart(2, "0");
return showSeconds ? `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}` : `${yyyy}-${MM}-${dd} ${HH}:${mm}`;
}
function showMessagePopupWithHtmlAndCallbacks(title, messageAsHtml, callback, cancelBtnCallback) {
$("#popupTitle").text(title);
if(messageAsHtml !== null) {
$("#modal-body").html("<p id=\"popupMessage\"></p>");
}
$("#popupMessage").html(messageAsHtml);
$("#message-popup-ok-btn").off("click");
if(typeof callback === "function") {
$("#message-popup-ok-btn").on("click", function() {
callback();
});
}
if(typeof cancelBtnCallback === "function") {
$("#message-popup-close-btn").off("click");
$("#message-popup-close-btn").on("click", function() {
cancelBtnCallback();
});
}
$("#messagePopup").modal(
{
backdrop: "static",
keyboard: false
}
);
}

View File

@@ -625,5 +625,12 @@ namespace BeWoPlanerMobil.Util
return "5px solid #C0FFD0";
}
public static string GetCssRightValue(DevExpress.XtraScheduler.Appointment appointment)
{
var serviceRecords = appointment.CustomFields[nameof(SchedulerAppointmentDC.ServiceRecordList)] as List<ServiceRecordDC> ?? new List<ServiceRecordDC>();
return serviceRecords.Any() ? "40px" : "23px";
}
}
}

View File

@@ -1079,7 +1079,7 @@
}
</div>
@if(Model.HasRightToViewCustomerAbsenceTimes)
@if(Model.HasRightToViewCustomerAbsenceTimes && Model.SelectedCustomer != null)
{
<div class="modal" tabindex="-1" role="dialog" id="customer-absence-times-form-popup">
<div class="modal-dialog modal-lg" role="document">

View File

@@ -118,6 +118,23 @@
console.error(error);
}
}
function OnAppointmentMenuPopup(s, e) {
const selectedAppointment = scheduler.GetAppointmentById(scheduler.GetSelectedAppointmentIds()[0]);
console.log(selectedAppointment);
for(let i = 0; i < e.item.items.length; i++) {
const currentItem = e.item.items[i];
if(currentItem.name === "ToServiceRecord") {
const isTask = selectedAppointment.customField("IsTask");
const isAbsenceTime = selectedAppointment.customField("IsAbsenceTime");
if(isTask === true || isAbsenceTime === true) {
currentItem.SetVisible(false);
}
}
}
}
</script>
@using(Html.BeginForm())

View File

@@ -1,4 +1,5 @@
@using BeWoPlanerMobil.Util
@using BS.Shared.DataContracts
@model DevExpress.XtraScheduler.Appointment
@functions
@@ -37,16 +38,45 @@
margin-right: 0;
}
.horizonta.horizontal-appointment-label {
.horizontal-appointment-label {
display: inline-block;
margin-right: 5px;
margin-left: 0.25em;
margin-top: 0.25em;
}
.horizontal-recurring-label-span {
margin-top: 5px;
margin-right: 10px;
right: 10px;
}
</style>
<div class="horizontal-appointment-template-div" style="border-left: @MobileUtils.GetLeftAppointmentBorder(Model);background: @MobileUtils.GetAppointmentRightBackground(Model) right / 5px 100% no-repeat, #C0FFD0;">
<div class="horizontal-appointment-info">
<label class="horizontal-appointment-label">
@GetHeaderTime() <span style="font-weight: bold;">@GetSubjectAndLocation()</span>
</label>
<label class="horizontal-appointment-label">
@GetHeaderTime() <span style="font-weight: bold;">@GetSubjectAndLocation()</span>
</label>
<div class="clearfix d-inline">
@if(Model.CustomFields[nameof(SchedulerAppointmentDC.ServiceRecordList)] is List<ServiceRecordDC> serviceRecords && serviceRecords.Any())
{
<i class="fas fa-clock text-bewo-service-records float-right horizontal-recurring-label-span mr-1"></i>
}
@if(Model.IsRecurring)
{
if(Model.Id is long)
{
<span class="fa-stack fa-1x text-info float-right" style="height:fit-content;width:fit-content;position:absolute;top:10px;right:@MobileUtils.GetCssRightValue(Model);">
<i class="fas fa-solid fa-stack-1x fa-slash"></i>
<i class="fas fa-solid fa-stack-1x fa-sync-alt"></i>
</span>
}
else
{
<i class="fas fa-sync-alt text-info float-right mr-1 horizontal-recurring-label-span"></i>
}
}
</div>
</div>
</div>

View File

@@ -58,6 +58,8 @@
e.Menu.Items.Add(new MenuItem("Verfügbare Mitarbeiter anzeigen", "ShowAvailableEmployees"));
}
e.Menu.ClientSideEvents.PopUp = "OnAppointmentMenuPopup";
foreach(MenuItem item in e.Menu.Items)
{
if(invisibleMenuItemNames.Contains(item.Name))
@@ -160,6 +162,7 @@
settings.OptionsToolTips.ShowSelectionToolTip = false;
settings.ClientSideEvents.ActiveViewChanging = "onActiveViewChanged";
settings.ClientSideEvents.MenuItemClicked = "onMenuItemClicked";
settings.PopupMenuShowing = PopupMenuShowing;
settings.Storage.Appointments.Assign(SchedulerHelper.DefaultAppointmentStorage);

View File

@@ -4,15 +4,9 @@
@model AppointmentViewInfo
@functions{
string GetHeaderText(){
return $"{Model.AppointmentInterval.Start.ToShortTimeString()} - {Model.Appointment.End.ToShortTimeString()}";
}
string GetRight()
string GetHeaderText()
{
var serviceRecords = Model.Appointment.CustomFields[nameof(SchedulerAppointmentDC.ServiceRecordList)] as List<ServiceRecordDC> ?? new List<ServiceRecordDC>();
return serviceRecords.Any() ? "40px" : "23px";
return $"{Model.AppointmentInterval.Start.ToShortTimeString()} - {Model.Appointment.End.ToShortTimeString()}";
}
}
@@ -65,7 +59,7 @@
{
if(Model.Appointment.Id is long)
{
<span class="fa-stack fa-1x text-info float-right" style="height:fit-content;width:fit-content;position:absolute;top:10px;right:@GetRight();">
<span class="fa-stack fa-1x text-info float-right" style="height:fit-content;width:fit-content;position:absolute;top:10px;right:@MobileUtils.GetCssRightValue(Model.Appointment);">
<i class="fas fa-solid fa-stack-1x fa-slash"></i>
<i class="fas fa-solid fa-stack-1x fa-sync-alt"></i>
</span>

View File

@@ -35,17 +35,8 @@
let validationMessage = "";
const allDay = $(`#@Model.HtmlPrefix-absence-time-allday-cb`).is(":checked");
const startDateString = $(`#@Model.HtmlPrefix-absence-time-start-date-input`).val();
const endDateString = $(`#@Model.HtmlPrefix-absence-time-end-date-input`).val(); // z.B. 2025-05-20
const startTimeString = $(`#@Model.HtmlPrefix-absence-time-start-time-input`).val(); // z.B. 09:17
const endTimeString = $(`#@Model.HtmlPrefix-absence-time-end-time-input`).val();
const isInfinite = endDateString.length === 0 && endTimeString.length === 0;
const start = allDay ? new Date(startDateString) : combineDateAndTime(new Date(startDateString), startTimeString);
const end = allDay ? new Date(endDateString) : combineDateAndTime(new Date(endDateString), endTimeString);
const startAndEnd = getStartAndEndAndIfInfinite();
const [start, end, isInfinite] = startAndEnd;
const isStartValid = false === isNaN(start.getTime());
const isEndValid = false === isNaN(end.getTime()) || isInfinite;
@@ -74,6 +65,22 @@
}
}
function getStartAndEndAndIfInfinite() {
const allDay = $(`#@Model.HtmlPrefix-absence-time-allday-cb`).is(":checked");
const startDateString = $(`#@Model.HtmlPrefix-absence-time-start-date-input`).val();
const endDateString = $(`#@Model.HtmlPrefix-absence-time-end-date-input`).val(); // z.B. 2025-05-20
const startTimeString = $(`#@Model.HtmlPrefix-absence-time-start-time-input`).val(); // z.B. 09:17
const endTimeString = $(`#@Model.HtmlPrefix-absence-time-end-time-input`).val();
const isInfinite = endDateString.length === 0 && endTimeString.length === 0;
const start = allDay ? new Date(`${startDateString}T00:00`) : combineDateAndTime(new Date(startDateString), startTimeString);
const end = allDay ? new Date(`${endDateString}T00:00`) : combineDateAndTime(new Date(endDateString), endTimeString);
return [start, end, isInfinite];
}
function toggleAllDay() {
try {
const checkbox = $(`#@Model.HtmlPrefix-absence-time-allday-cb`);
@@ -100,9 +107,47 @@
return;
}
showSpinner();
$("#create-absence-time-form").submit();
$(`#@Model.HtmlPrefix-absence-times-form-popup`).modal("hide");
const [start, end] = getStartAndEndAndIfInfinite();
$.get("@Url.Action("CheckForOverlappingAbsenceTimes", controllerName)",
{
startStr: getUnixTimeStamp(start, true, false),
endStr: getUnixTimeStamp(end, true, false),
oid: @Model.AbsenteeOid
}
).done(
function(messagesJson) {
const messages = JSON.parse(messagesJson);
if(messages.length === 0) {
showSpinner();
$("#create-absence-time-form").submit();
$(`#@Model.HtmlPrefix-absence-times-form-popup`).modal("hide");
} else {
$(`#@Model.HtmlPrefix-absence-times-form-popup`).modal("hide");
let messagesStr = "";
for(let i = 0; i < messages.length; i++) {
messagesStr += messages[i];
}
showMessagePopupWithHtmlAndCallbacks(
"BeWoPlaner",
messagesStr,
() => {
showSpinner();
$("#create-absence-time-form").submit();
$(`#@Model.HtmlPrefix-absence-times-form-popup`).modal("hide");
},
() => {
$("#messagePopup").modal("hide");
$(`#@Model.HtmlPrefix-absence-times-form-popup`).modal("show");
}
);
}
}
);
} catch(error) {
showErrorPopup(error);
}

View File

@@ -53,7 +53,7 @@
<script type="text/javascript" src="@Scripts.Url("~/Scripts/moment/moment.min.js")"></script>
<script type="text/javascript" src="@Scripts.Url("~/Scripts/moment/locale/de.js")"></script>
<script type="text/javascript" src="@Scripts.Url("~/Scripts/ownSoft-Scripts/mobileUtils.js?v=1.8")"></script>
<script type="text/javascript" src="@Scripts.Url("~/Scripts/ownSoft-Scripts/mobileUtils.js?v=1.9")"></script>
<script type="text/javascript" src="@Scripts.Url("~/Scripts/ownSoft-Scripts/view-scripts/main.js?v=2.6")"></script>
<script type="text/javascript" src="@Scripts.Url("~/Scripts/ownSoft-Scripts/signature.js?v=1.5")"></script>
@@ -375,7 +375,7 @@
{
<li class="nav-item">
<button class="btn btn-link text-light no-underline" onclick="showSpinner()">
<i class="fas fa-hammer" style="color: #FFD9C2;"></i>
<i class="far fa-calendar-alt text-bewo-calendar"></i>
DX Kalender
</button>
</li>