Kalender: Tooltip erweitert und Bugs behoben.

This commit is contained in:
2026-07-16 16:01:13 +02:00
parent 5752537e96
commit 1f853494ad
10 changed files with 294 additions and 70 deletions

View File

@@ -193,6 +193,15 @@ namespace BeWoPlanerMobil.Controllers
var appointments = allAppointments;
var changedOrDeletedOccurrences = KalenderService.FindAppointmentsByRecurrenceId(appointments.Select(apt => apt.RecurrenceId).ToList(), true);
Model.RecurrenceIdsWithOccurrences = new List<string>();
foreach(var occ in changedOrDeletedOccurrences)
{
Model.RecurrenceIdsWithOccurrences.AddIfNotIn(occ.RecurrenceIdReference);
}
foreach(var appointment in appointments)
{
if(false == appointment.IsPrivate || appointment.Originator.EmployeeOid.Equals(employeeOid) || appointment.EmployeeList.Any(e => e.Employee.EmployeeOid.Equals(employeeOid)))
@@ -489,9 +498,8 @@ namespace BeWoPlanerMobil.Controllers
foreach(var appointment in appointmentsToInsert)
{
if(appointment.Type != (int) AppointmentType.DeletedOccurrence)
if(appointment.Type != (int) AppointmentType.DeletedOccurrence && appointment.Type != (int) AppointmentType.ChangedOccurrence)
{
// Beim Löschen eines Serientermins wird hier ein Insert für die gelöschte Ausnahme erstellt
appointment.CustomerList = AppointmentModel.SelectedCustomers;
appointment.ResourceList = AppointmentModel.SelectedResources;
appointment.EmployeeList = new List<Employee2SchedulerAppointmentDC>();
@@ -580,14 +588,38 @@ namespace BeWoPlanerMobil.Controllers
case "Reload":
ReloadAppointments();
break;
case "debug_DummyAppointments":
CreateDummyAppointments();
case "DeleteSeries":
DeleteAppointment(((List<SchedulerAppointmentDC>) Model.Appointments).FirstOrDefault(appointment => appointment.SchedulerAppointmentOid == long.Parse(apptID)));
break;
case "DeleteApt":
DeleteAppointment(((List<SchedulerAppointmentDC>) Model.Appointments).FirstOrDefault(appointment => appointment.SchedulerAppointmentOid == long.Parse(apptID)));
break;
case "ResetSeries":
var oid2Versions = KalenderService.LoadOccurrencesByRecurrenceId(apptID).Where(appointment => appointment.RecurrenceIndexReference != null).ToDictionary(x => x.SchedulerAppointmentOid.Value, y => y.NewSchedulerAppointmentVersion.Value);
KalenderService.DeleteSchedulerAppointments(oid2Versions);
ReloadAppointments();
break;
}
return PartialView("SchedulerPagePartial", Model);
}
private void DeleteAppointment(SchedulerAppointmentDC appointment2Delete)
{
if(appointment2Delete is null || appointment2Delete.SchedulerAppointmentOid is null || appointment2Delete.NewSchedulerAppointmentVersion is null)
{
return;
}
var oid2Version = new Dictionary<long, long> { { appointment2Delete.SchedulerAppointmentOid.Value, appointment2Delete.NewSchedulerAppointmentVersion.Value } };
KalenderService.DeleteSchedulerAppointments(oid2Version);
ReloadAppointments();
}
private void CreateDummyAppointments()
{
var today = DateTime.Today;

View File

@@ -25,6 +25,8 @@ namespace BeWoPlanerMobil.Models
set => DevExpressSchedulerSessionModel.CurrentSchedulerViewType = value;
}
public List<string> RecurrenceIdsWithOccurrences { get; set; }
public DevExpressSchedulerSessionModel DevExpressSchedulerSessionModel => SessionModel as DevExpressSchedulerSessionModel;
public List<CompactCustomerDC> PossibleCustomers { get; set; }

View File

@@ -28,6 +28,16 @@ namespace BeWoPlanerMobil.Util.SchedulerUtils
return "Monat";
case SchedulerStringId.ViewShortDisplayName_Month:
return "Monat";
case SchedulerStringId.Caption_WeekOfMonthFirst:
return "Ersten";
case SchedulerStringId.Caption_WeekOfMonthSecond:
return "Zweiten";
case SchedulerStringId.Caption_WeekOfMonthThird:
return "Dritten";
case SchedulerStringId.Caption_WeekOfMonthFourth:
return "Vierten";
case SchedulerStringId.Caption_WeekOfMonthLast:
return "Letzten";
}
return base.GetLocalizedString(id);

View File

@@ -1,7 +1,7 @@
<script type="text/javascript">
function onAppointmentTipDisplaying(s, e) {
const currentApt = getCurrentApt();
console.log(currentApt);
if(currentApt.updated === false) {
scheduler.RefreshClientAppointmentProperties(currentApt, AppointmentPropertyNames.Normal, OnAppointmentRefresh);
setAptToolTipSubject("Lade...");
@@ -12,7 +12,7 @@
const participationInfos = currentApt.ParticipationInfos;
const resourceInfos = currentApt.ResourceInfos;
const customerInfos = currentApt.CustomerInfos;
console.log(currentApt); // ToDo: Icon bei Serientermin! Index auch?
let bgClass = "";
$("#tool-tip-info").empty();
@@ -72,10 +72,23 @@
if(currentApt.CanBeEdited === false) {
if(currentApt.CanBeViewed) {
$("#aptToolTipEdit").text("Ansehen");
$("#aptToolTipEditSeries").text("Serie Ansehen");
} else {
$("#aptToolTipEdit").hide();
$("#aptToolTipEdit, #aptToolTipEditSeries").hide();
}
}
if(currentApt.HasOccurrencies === false) {
$("#reset-series-list-item").hide();
} else {
$("#reset-series-list-item").show();
}
if(currentApt.RecurrenceId === null || currentApt.RecurrenceId === undefined) {
$("#aptToolTipEditSeries").hide();
} else {
$("#aptToolTipEditSeries").show();
}
}
function setAptToolTipSubject(text) {
@@ -92,15 +105,58 @@
}
function onAptToolTipDeleteClick() {
scheduler.DeleteAppointment(GetCurrentApt());
const appointment = getCurrentApt();
if(appointment.appointmentType === "Occurrence") {
$("#apt-del-ok-btn").on("click",
function() {
const radioOptionId = $("input[name='apt-del-radio-group']:checked").attr("id");
if(radioOptionId === "apt-del-radio-1") {
const patternId = appointment.appointmentId.split("_")[0];
scheduler.PerformCallback({apptID: patternId, actionId: "DeleteSeries", args: null}, () => { hideSpinner(); });
} else if(radioOptionId === "apt-del-radio-2") {
scheduler.DeleteAppointment(appointment);
}
$("#apt-del-ok-btn").off("click");
$("#recurrence-delete-popup").modal("hide");
});
$("#recurrence-delete-popup").modal("show");
} else {
showWarningMessageBox("Löschen bestätigen", "Sind Sie sicher, dass Sie den Termin löschen möchten?", () => {
scheduler.DeleteAppointment(appointment);
}, false);
}
}
function onAptToolTipEditClick() {
function onAptToolTipEditClick() {
scheduler.ShowAppointmentFormByClientId(getCurrentApt().GetId());
}
function onAptToolTipEditSeriesClick() {
scheduler.ShowAppointmentFormByClientId(getCurrentApt().GetId().split("_")[0]);
}
function onAptToolTipShowMenuClick() {
toolTipContext.toolTip.ShowAppointmentMenu();
}
function onAptToolTipTransferClick() {
scheduler.PerformCallback({apptID: getCurrentApt().appointmentId, actionId: "ToServiceRecord" });
}
function onAptToolTipResetClick() {
const appointment = getCurrentApt();
const recurrenceId = appointment.RecurrenceId;
if(recurrenceId !== undefined && recurrenceId !== null && recurrenceId.length > 0) {
showWarningMessageBox("Zurücksetzen der Serie bestätigen", "Sind Sie sicher, dass Sie die ausgewählte Terminserie zurücksetzen möchten?", () => {
scheduler.PerformCallback({ apptID: recurrenceId, actionId: "ResetSeries", args: null }, () => { hideSpinner(); });
}, false);
}
}
</script>
@@ -115,11 +171,32 @@
</div>
</div>
<div class="card-footer m-0 p-1">
<button type="button" id="aptToolTipEdit" class="btn btn-sm btn-primary" onclick="onAptToolTipEditClick()">
Bearbeiten
</button>
<button type="button" id="aptToolTipDelete" class="btn btn-sm btn-danger float-right" onclick="onAptToolTipDeleteClick()">
Löschen
</button>
<ul class="list-group">
<li class="list-group-item p-1">
<button type="button" id="aptToolTipEdit" class="btn btn-sm btn-primary w-100" onclick="onAptToolTipEditClick()">
Bearbeiten
</button>
</li>
<li class="list-group-item p-1">
<button type="button" id="aptToolTipEditSeries" class="btn btn-sm btn-primary w-100" onclick="onAptToolTipEditSeriesClick()">
Serie Bearbeiten
</button>
</li>
<li class="list-group-item p-1">
<button type="button" id="aptToolTipDelete" class="btn btn-sm btn-danger w-100" onclick="onAptToolTipDeleteClick()">
Löschen
</button>
</li>
<li class="list-group-item p-1">
<button type="button" id="aptToolTipTransfer" class="btn btn-sm btn-primary w-100" onclick="onAptToolTipTransferClick()">
In die Zeiterfassung übertragen
</button>
</li>
<li class="list-group-item p-1" id="reset-series-list-item">
<button type="button" id="aptToolTipReset" class="btn btn-sm btn-primary w-100" onclick="onAptToolTipResetClick()">
Serie zurücksetzen
</button>
</li>
</ul>
</div>
</div>

View File

@@ -13,30 +13,22 @@
newAptBtn.empty();
newAptBtn.append("<span>Neuer Termin</span>");
}
const observer = new MutationObserver((mutations, obs) => {
const el0 = document.getElementById("appointmentRecurrenceForm_AptRecCtl_Monthly_WmeWeekOfMnth_DDD_L_LBI0T0");
const el1 = document.getElementById("appointmentRecurrenceForm_AptRecCtl_Monthly_WmeWeekOfMnth_DDD_L_LBI1T0");
const el2 = document.getElementById("appointmentRecurrenceForm_AptRecCtl_Monthly_WmeWeekOfMnth_DDD_L_LBI2T0");
const el3 = document.getElementById("appointmentRecurrenceForm_AptRecCtl_Monthly_WmeWeekOfMnth_DDD_L_LBI3T0");
const el4 = document.getElementById("appointmentRecurrenceForm_AptRecCtl_Monthly_WmeWeekOfMnth_DDD_L_LBI4T0");
const dropdown = document.getElementById("appointmentRecurrenceForm_AptRecCtl_Monthly_WmeWeekOfMnth_I");
if(el0 && el1 && el2 && el3 && el4 && dropdown) {
$(el0).text("Ersten");
$(el1).text("Zweiten");
$(el2).text("Dritten");
$(el3).text("Vierten");
$(el4).text("Letzten");
obs.disconnect();
}
});
observer.observe(document.body, {childList: true, subtree: true});
});
function translateRecurrencePopup() {
const popupTitle = document.getElementById("scheduler_formBlock_RecurAptDeleteFrmContainer_PWH-1T");
const msgSpan = document.getElementById("scheduler_formBlock_RecurAptDeleteFrmContainer_RecurAptDeleteFrmTemplateContainer_RecurrentAppointmentDeleteForm_lblConfirm");
const checkboxTable = document.getElementById("scheduler_formBlock_RecurAptDeleteFrmContainer_RecurAptDeleteFrmTemplateContainer_RecurrentAppointmentDeleteForm_rbAction_RB0");
const checkbox2Table = document.getElementById("scheduler_formBlock_RecurAptDeleteFrmContainer_RecurAptDeleteFrmTemplateContainer_RecurrentAppointmentDeleteForm_rbAction_RB1");
const cancelBtnDiv = document.getElementById("scheduler_formBlock_RecurAptDeleteFrmContainer_RecurAptDeleteFrmTemplateContainer_RecurrentAppointmentDeleteForm_btnCancel_CD");
$(msgSpan).text("Möchten Sie diesen Termin der Serie oder die ganze Serie löschen?");
popupTitle.textContent = "Löschen bestätigen";
checkboxTable.querySelector("td label").textContent = "Die Serie";
checkbox2Table.querySelector("td label").textContent = "Diesen Termin";
cancelBtnDiv.querySelector("span").textContent = "Abbrechen";
}
function UpdateSchedulerHeight() {
scheduler.SetHeight(1);
@@ -204,6 +196,52 @@
function onMenuItemClicked(s, e) {
try {
if(e.itemName === "ResetRecurringApt") {
e.handled = true;
const oid = scheduler.GetSelectedAppointmentIds()[0];
const appointment = scheduler.GetAppointmentById(oid);
const recurrenceId = appointment.RecurrenceId;
if(recurrenceId !== undefined && recurrenceId !== null && recurrenceId.length > 0) {
showWarningMessageBox("Zurücksetzen der Serie bestätigen", "Sind Sie sicher, dass Sie die ausgewählte Terminserie zurücksetzen möchten?", () => {
scheduler.PerformCallback({ apptID: recurrenceId, actionId: "ResetSeries", args: null }, () => { hideSpinner(); });
}, false);
}
}
if(e.itemName === "DeleteApt") {
e.handled = true;
const oid = scheduler.GetSelectedAppointmentIds()[0];
const appointment = scheduler.GetAppointmentById(oid);
if(appointment.appointmentType === "Occurrence") {
$("#apt-del-ok-btn").on("click",
function() {
const radioOptionId = $("input[name='apt-del-radio-group']:checked").attr("id");
if(radioOptionId === "apt-del-radio-1") {
const patternId = appointment.appointmentId.split("_")[0];
scheduler.PerformCallback({apptID: patternId, actionId: "DeleteSeries", args: null}, () => { hideSpinner(); });
} else if(radioOptionId === "apt-del-radio-2") {
scheduler.DeleteAppointment(appointment);
}
$("#apt-del-ok-btn").off("click");
$("#recurrence-delete-popup").modal("hide");
});
$("#recurrence-delete-popup").modal("show");
} else {
showWarningMessageBox("Löschen bestätigen", "Sind Sie sicher, dass Sie den Termin löschen möchten?", () => {
scheduler.PerformCallback({apptID: oid, actionID: e.itemName});
}, false);
}
}
if(e.itemName === "ToServiceRecord") {
e.handled = true;
scheduler.PerformCallback({ apptID: scheduler.GetSelectedAppointmentIds()[0], actionId: e.itemName });
@@ -236,6 +274,11 @@
const currentItem = e.item.items[i];
switch(currentItem.name) {
case "ResetRecurringApt":
if(selectedAppointment.IsAbsenceTime || selectedAppointment.IsTask || selectedAppointment.HasOccurrencies === false) {
currentItem.SetVisible(false);
}
break;
case "ToServiceRecord":
if(id === undefined || selectedAppointment.IsAbsenceTime || selectedAppointment.IsTask) {
currentItem.SetVisible(false);
@@ -799,3 +842,31 @@
@Html.Partial("AptFltCustomersPartial", Model)
@Html.Partial("AptFltResourcesPartial", Model)
<div class="modal" tabindex="-1" role="dialog" id="recurrence-delete-popup">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header py-1 px-2">
Löschen bestätigen
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body p-2">
Möchten Sie die ganze Serie oder nur diesen Termin löschen?
<div class="form-check">
<input type="radio" class="form-check-input" name="apt-del-radio-group" id="apt-del-radio-1">
<label for="apt-del-radio-1" class="form-check-label">Die Serie</label>
</div>
<div class="form-check">
<input type="radio" class="form-check-input" name="apt-del-radio-group" id="apt-del-radio-2" checked>
<label for="apt-del-radio-2" class="form-check-label">Diesen Termin</label>
</div>
</div>
<div class="modal-footer py-1 px-2">
<button type="button" class="btn btn-primary" id="apt-del-ok-btn">OK</button>
<button type="button" class="btn btn-primary" data-dismiss="modal">Abbrechen</button>
</div>
</div>
</div>
</div>

View File

@@ -1,4 +1,5 @@
@using BeWoPlanerMobil.Models
@using System.Diagnostics
@using BeWoPlanerMobil.Models
@using BeWoPlanerMobil.Util.SchedulerUtils
@using BeWoPlanerMobil.Views.DevExpressScheduler
@using BS.Shared.DataContracts
@@ -10,7 +11,7 @@
@functions
{
//ToDo: So das EditForm verändern!
//ToDo: So das EditForm verändern:
//https://docs.devexpress.com/AspNetMvc/119776/components/scheduler/get-started/how-to-customize-the-appointment-dialog-using-view-model-api-working-with-custom-fields?utm_source=SupportCenter&utm_medium=website&utm_campaign=docs-feedback&utm_content=T743814
static AppointmentRecurrenceFormSettings CreateAppointmentRecurrenceFormSettings(CustomAppointmentTemplateContainer container)
@@ -45,13 +46,15 @@
"StatusSubMenu",
"LabelSubMenu",
"NewAllDayEvent",
"NewRecurringEvent"
"NewRecurringEvent",
"DeleteAppointment"
};
if(menuId == SchedulerMenuItemId.AppointmentMenu)
{
e.Menu.Items.Add(new MenuItem("Löschen", "DeleteApt"));
e.Menu.Items.Add(new MenuItem("In die Zeiterfassung übertragen", "ToServiceRecord"));
e.Menu.Items.Add(new MenuItem("Serie zurücksetzen", "ResetRecurringApt"));
e.Menu.Items.Add(new MenuItem("Zusagen", "ConfirmApt"));
e.Menu.Items.Add(new MenuItem("Mit Vorbehalt zusagen", "MaybeApt"));
e.Menu.Items.Add(new MenuItem("Absagen", "CancelApt"));
@@ -221,6 +224,14 @@
settings.InitClientAppointment = (send, evargs) =>
{
// TODO: TESTEN! Es scheint noch einen Bug zu geben!
var recurrenceId = evargs.Appointment.RecurrenceInfo?.Id?.ToString();
var index = evargs.Appointment.RecurrenceIndex;
evargs.Properties.Add("RecurrenceId", recurrenceId);
evargs.Properties.Add("HasOccurrencies", recurrenceId is string ? Model.RecurrenceIdsWithOccurrences.Contains(recurrenceId) : false);
var isTask = evargs.Appointment.CustomFields["IsTask"] is true;
var isAbsenceTime = evargs.Appointment.CustomFields["IsAbsenceTime"] is true;
var canBeEdited = evargs.Appointment.CustomFields["CanBeEdited"] is true;
@@ -253,40 +264,40 @@
foreach(var employee2Appointment in employeeList)
{
if(employee2Appointment.SchedulerAppointmentOid is null || employee2Appointment.Employee2SchedulerAppointmentOid is null)
{
continue;
}
if(employee2Appointment.SchedulerAppointmentOid is null || employee2Appointment.Employee2SchedulerAppointmentOid is null)
{
continue;
}
var parcitipationInfoObject =
new ParticipationInfoObject(
employee2Appointment.Employee.DetailDescription,
employee2Appointment.Employee2SchedulerAppointmentOid.Value,
(int) employee2Appointment.ParticipationAnswer,
employee2Appointment.SchedulerAppointmentOid.Value);
var parcitipationInfoObject =
new ParticipationInfoObject(
employee2Appointment.Employee.DetailDescription,
employee2Appointment.Employee2SchedulerAppointmentOid.Value,
(int) employee2Appointment.ParticipationAnswer,
employee2Appointment.SchedulerAppointmentOid.Value);
participationInfos.Add(parcitipationInfoObject);
participationInfos.Add(parcitipationInfoObject);
}
foreach(var resource in resourceList)
{
var resourceInfoObject =
new ResourceInfoObject(
oid,
resource.Name,
resource.Color
);
var resourceInfoObject =
new ResourceInfoObject(
oid,
resource.Name,
resource.Color
);
resourceInfos.Add(resourceInfoObject);
}
foreach(var customer in customerList)
{
var customerInfoObject =
new CustomerInfoObject(
oid,
customer.DetailDescription
);
var customerInfoObject =
new CustomerInfoObject(
oid,
customer.DetailDescription
);
customerInfos.Add(customerInfoObject);
}
@@ -295,6 +306,7 @@
evargs.Properties.Add("ResourceInfos", resourceInfos);
evargs.Properties.Add("CustomerInfos", customerInfos);
// originator ist bei einer Aufgabe null
var canConfirm = (false == isTask || false == isAbsenceTime) && end > DateTime.Now &&
false == (originator?.EmployeeOid.Equals(Model.LoggedInUserOid) ?? false) &&
@@ -308,6 +320,7 @@
settings.Storage.Appointments.Assign(SchedulerHelper.DefaultAppointmentStorage);
settings.Storage.EnableReminders = false;
settings.OptionsBehavior.ClientTimeZoneId = "W. Europe Standard Time";
settings.Width = Unit.Percentage(100);
settings.Height = Unit.Percentage(100);

View File

@@ -6815,7 +6815,6 @@ WHERE sc.Billable = 1 and sr.StartDate >= '{0:yyyy-MM-dd}' and sr.StartDate < '{
return criteria.List<TeamFolderRight>().ToList();
}
}
}

View File

@@ -162,6 +162,7 @@
</system.Web>
-->
<system.web>
<globalization uiCulture="de" culture="de" />
<compilation debug="true" defaultLanguage="c#" targetFramework="4.5">
<assemblies>
<add assembly="BeWo.Service, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null" />

View File

@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.ServiceModel;
using BeWo.Data.Entities;
using BS.Shared.Core;
using BS.Shared.DataContracts;
using System;
using System.Collections.Generic;
using System.ServiceModel;
namespace BeWo.Service.ServiceContracts
{
@@ -276,5 +277,9 @@ namespace BeWo.Service.ServiceContracts
[FaultContract(typeof(BeWoFault))]
[OperationContract]
List<SchedulerAppointmentDC> LoadFilteredAppointmentsMitAufgabenForMoK(bool pHasRightToSeeAllEmployeeAppointments, long pEmployeeOid, DateTime pIntervalStart, DateTime pIntervalEnd, List<long> pSelectedEmployees, List<long> pSelectedCustomer, List<long> pSelectedResources, bool pEmployeesOnly, bool pCustomersOnly, bool pResourcesOnly, bool pPrivateAppointmentsOnly, bool pOnlyMyAppointments, bool pShowTasks);
[FaultContract(typeof(BeWoFault))]
[OperationContract]
List<SchedulerAppointmentDC> FindAppointmentsByRecurrenceId(List<string> pRecurrenceIds, bool pExcludeRootAppointments = false);
}
}

View File

@@ -3146,5 +3146,19 @@ namespace BeWo.Service.ServiceImplementations
throw Utils.CreateBeWoFaultException(e);
}
}
public List<SchedulerAppointmentDC> FindAppointmentsByRecurrenceId(List<string> recurrenceIds, bool excludeRootAppointments = false)
{
try
{
var occurrences = DAOFactory.SearchDAO.FindAppointmentsByRecurrenceId(recurrenceIds, excludeRootAppointments);
return MapperFactory.SchedulerAppointmentDCSchedulerAppointment.MapToNewDCs(occurrences);
}
catch(Exception e)
{
throw Utils.CreateBeWoFaultException(e);
}
}
}
}