Merge branch 'master' of ssh://float.ownsoft.de/git/beyondSoft/BeWo

This commit is contained in:
2026-04-15 11:30:03 +02:00
237 changed files with 22536 additions and 3492 deletions

View File

@@ -413,6 +413,8 @@
<Content Include="Content\datepicker_jQM.css" />
<Content Include="Content\dx.common.css" />
<Content Include="Content\dx.light.css" />
<Content Include="Content\images\Icon_Mikro_Schw.png" />
<Content Include="Content\images\Icon_Mikro_Weiss.png" />
<Content Include="Scripts\bootstrap\bootstrap.bundle.min.js" />
<Content Include="Scripts\bootstrap\bootstrap4-modal-fullscreen.min.css" />
<Content Include="Scripts\bs-custom-file-input.min.js" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

@@ -146,7 +146,7 @@ namespace BeWoPlanerMobil.Controllers
return;
}
if(customerOid == -1)
if(customerOid <= 0)
{
Model.CustomerSessionModel.ResetValues();
}

View File

@@ -16,6 +16,7 @@ using DevExpress.XtraScheduler;
using DevExpress.XtraScheduler.Compatibility;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
@@ -31,6 +32,7 @@ using FormCollection = System.Web.Mvc.FormCollection;
using GoalRating = BeWoPlanerMobil.Util.GoalRating;
using Image = System.Drawing.Image;
using ServiceRecordDC = BS.Shared.DataContracts.ServiceRecordDC;
using BeWo.Service.AI;
/*
* ToDo: Bug, wenn man auf "bearbeiten" bei einem Eintrag klickt, wird die Leistung nicht geladen
@@ -41,6 +43,13 @@ namespace BeWoPlanerMobil.Controllers
{
public class MainController : SignableController
{
// Speichert den Status externer Aufrufe (callId -> Callback-Daten)
private static readonly ConcurrentDictionary<string, string> _externalCallResults = new ConcurrentDictionary<string, string>();
// Speichert optionale Statusmeldungen für laufende externe Aufrufe (callId -> Statustext)
private static readonly ConcurrentDictionary<string, string> _externalCallStatusMessages = new ConcurrentDictionary<string, string>();
// Speichert das Aufnametoken für laufende externe Aufrufe (callId -> Token)
private static readonly ConcurrentDictionary<string, AiVoiceDataDC> _externalCallTokens = new ConcurrentDictionary<string, AiVoiceDataDC>();
public MainController()
{
int test = 0;
@@ -3553,6 +3562,160 @@ namespace BeWoPlanerMobil.Controllers
return View("Main", Model);
}
/// <summary>
/// Ermittelt die externe URL, die beim Klick auf den Test-Button aufgerufen werden soll.
/// Die callbackUrl wird als Parameter mitgegeben, damit das externe System sie verwenden kann.
/// </summary>
[HttpGet]
[Authorize]
public JsonResult GetExternalCallUrl(string callId, string callbackUrl)
{
try
{
var externalUrl = "";
if (Mandator?.OwnChatCode != null)
{
var ts = new TranscriptionService();
var avd = ts.InitTranskription(MobileSessionFacade.Tenant, false);
externalUrl = avd.OwnChatUrl;
if (!string.IsNullOrEmpty(callId) && !string.IsNullOrEmpty(avd.Token))
{
_externalCallTokens[callId] = ts.MapAiVoiceDataDC(avd);
}
Log.Info($"GetExternalCallUrl: {callId}. Url = {externalUrl}, Transkription startet!");
//var externalBaseUrl = "https://example.com/external-service";
//var externalUrl = externalBaseUrl
// + "?callbackUrl=" + Uri.EscapeDataString(callbackUrl)
// + "&callId=" + Uri.EscapeDataString(callId);
}
else
{
Log.Info($"GetExternalCallUrl: {callId}. Url = {callbackUrl}, Transkription starten hat nicht geklappt. Kein OwnChatCode vorhanden!");
}
return Json(new { success = true, url = externalUrl }, JsonRequestBehavior.AllowGet);
}
catch(Exception ex)
{
return Json(new { success = false, message = "Fehler beim Ermitteln der externen URL: " + ex.Message }, JsonRequestBehavior.AllowGet);
}
}
/// <summary>
/// Callback-Endpunkt, der vom externen System aufgerufen wird.
/// Markiert den externen Aufruf als abgeschlossen.
/// </summary>
[HttpGet]
[AllowAnonymous]
public ActionResult ExternalCallCallback(string callId, string data = null)
{
if(!string.IsNullOrEmpty(callId))
{
_externalCallResults[callId] = data ?? string.Empty;
_externalCallStatusMessages.TryRemove(callId, out _);
_externalCallTokens.TryRemove(callId, out _);
}
return Content("OK");
}
/// <summary>
/// Kann vom externen System aufgerufen werden, um eine Statusmeldung zu setzen,
/// die dem Benutzer im Wartedialog angezeigt wird (ohne den Vorgang abzuschließen).
/// </summary>
[HttpGet]
[AllowAnonymous]
public ActionResult ExternalCallUpdateStatus(string callId, string message)
{
if(!string.IsNullOrEmpty(callId) && message != null)
{
_externalCallStatusMessages[callId] = message;
}
return Content("OK");
}
/// <summary>
/// Wird vom Browser per Polling aufgerufen, um zu prüfen ob der Callback bereits eingegangen ist.
/// Gibt bei completed=false optional eine Statusmeldung zurück.
/// </summary>
[HttpGet]
[Authorize]
public JsonResult CheckExternalCallStatus(string callId)
{
if(!string.IsNullOrEmpty(callId) && _externalCallResults.TryRemove(callId, out var data))
{
_externalCallStatusMessages.TryRemove(callId, out _);
_externalCallTokens.TryRemove(callId, out _);
return Json(new { completed = true, data }, JsonRequestBehavior.AllowGet);
}
string statusMessage = null;
//Log.Info($"CheckExternalCallStatus: Start {callId}.");
if (!string.IsNullOrEmpty(callId) && _externalCallTokens.TryRemove(callId, out var avddc))
{
var ts = new TranscriptionService();
var avd = ts.MapAiVoiceData(avddc);
avd = ts.GetTranskription(avd);
statusMessage = avd.StatusMessage;
if (!String.IsNullOrEmpty(avd.TranskriptionText))
{
statusMessage = "Transkription erfolgreich durchgeführt";
Log.Info($"CheckExternalCallStatus: {callId}. Transkription erfolgreich durchgeführt!");
ExternalCallCallback(callId, avd.TranskriptionText);
}
else if (!String.IsNullOrEmpty(avd.Error))
{
statusMessage = avd.Error;
Log.Info($"CheckExternalCallStatus: {callId}. Transkription mit Fehler beendet: {avd.Error}");
ExternalCallCallback(callId, "");
}
else
{
//Log.Info($"CheckExternalCallStatus: {callId}. Status: {statusMessage}");
}
avddc = ts.MapAiVoiceDataDC(avd);
_externalCallTokens[callId] = avddc;
}
else
{
Log.Info($"CheckExternalCallStatus: {callId}. Kein Token gefunden.");
}
return Json(new { completed = false, statusMessage }, JsonRequestBehavior.AllowGet);
}
/// <summary>
/// Wird aufgerufen, wenn der Benutzer den externen Aufruf abbricht.
/// Räumt den Eintrag auf, falls vorhanden.
/// </summary>
[HttpPost]
[Authorize]
public ActionResult CancelExternalCall(string callId)
{
if(!string.IsNullOrEmpty(callId))
{
_externalCallResults.TryRemove(callId, out _);
_externalCallStatusMessages.TryRemove(callId, out _);
_externalCallTokens.TryRemove(callId, out _);
}
return new HttpStatusCodeResult(200);
}
[HttpPost]
[Authorize]
public ActionResult ResetGroupBookingForm()

View File

@@ -58,11 +58,61 @@ namespace BeWoPlanerMobil.Models
public bool HasRightToInsertRessourceAppointments => CheckRight(UserRightType.KalenderRessourcentermineAnlegen) || CheckRight(UserRightType.CreateAll);
public bool HasRightToViewQuittierungsbelege
public bool HasRightToUseAiVoice
{
get
{
var showAiVoice = MobileUtils.GetSettingValue(Mandator.Settings, SettingsKeys.ShowAIVoice);
var showOwnChat = MobileUtils.GetSettingValue(Mandator.Settings, SettingsKeys.ShowChat);
#if DEBUG
showAiVoice = "1";
showOwnChat = "1";
//tenant = "4368658435";
//var serverUrl = OwnChatHelper.ErmittleServerURL("1", tenant);
//var apikey = "hNykpBwxE1Y6NmwZBTDQOpPXW6xCgnVHjJ5lh0QDQyCefgcnimC32aZ3CwB4qQl4";
//var userid = "1432790061";
Mandator.OwnChatCode = "HcYki-8hLbQ";
return true;
#endif
if ("1".Equals(showAiVoice) && CheckRight(UserRightType.AiVoiceView))
{
if (LoggedInEmployee != null && Mandator.OwnChatCode == null)
{
Mandator.OwnChatCode = "";
if ("1".Equals(showOwnChat))
{
var es = new BeWo.Service.ServiceImplementations.EmployeeServiceImp();
var empAppCodes = es.GetAllEmployeeAppCodes(LoggedInEmployee.EmployeeOid.Value);
if (empAppCodes != null && empAppCodes.Count > 0)
{
foreach (var item in empAppCodes)
{
if (item.EmployeeCode != null && !item.EmployeeCode.Equals(""))
Mandator.OwnChatCode = item.EmployeeCode;
}
}
}
}
}
return !String.IsNullOrEmpty(Mandator.OwnChatCode);
}
}
public bool HasRightToViewQuittierungsbelege
{
get
{
var showEmployeeSignatureSetting = MobileUtils.GetSettingValue(Mandator.Settings, SettingsKeys.ShowSignature);
#if DEBUG
showEmployeeSignatureSetting = "1";
@@ -72,7 +122,6 @@ namespace BeWoPlanerMobil.Models
}
public bool IsAllowedToSeeSupportConceptFilter
{
get

View File

@@ -18,6 +18,7 @@ namespace BeWoPlanerMobil.Models
//public string Website { get; set; }
//public string PostalCode { get; set; }
public string Settings { get; set; }
public string OwnChatCode { get; set; }
//public string State { get; set; }
//public string Street { get; set; }
//public string Town { get; set; }

View File

@@ -92,6 +92,7 @@ function changeGroupBookingFormEnableState(enableState) {
"#gb-distance-input, " +
"textarea, " +
"#reset-button, " +
"#aivoice-button, " +
"#create-button, " +
"#gb-textbausteine-btn, " +
"#gb-hours-minutes-dropdown-btn, " +
@@ -116,6 +117,7 @@ function changeSingleBookingFormEnableState(enableState) {
"#goals-button, " +
"textarea, " +
"#reset-button, " +
"#aivoice-button, " +
"#create-button, " +
"#statistics-button, " +
"#timeFrameSelect, " +

View File

@@ -168,6 +168,110 @@
}
</script>
<script type="text/javascript">
var _externalCallPollingTimer = null;
var _externalCallId = null;
function startExternalCall() {
// Eindeutige ID für diesen Aufruf generieren
_externalCallId = 'ext-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
// Callback-URL für das externe System
var callbackUrl = window.location.origin + '@Url.Action("ExternalCallCallback", "Main")' + '?callId=' + encodeURIComponent(_externalCallId);
// Externe URL vom Server ermitteln lassen
$.ajax({
url: '@Url.Action("GetExternalCallUrl", "Main")',
data: { callId: _externalCallId, callbackUrl: callbackUrl },
type: 'GET',
success: function(result) {
if (result && result.success && result.url) {
// Overlay anzeigen und Statustext zurücksetzen
$('#external-call-status-text').text('Um die Aufnahme zu starten, klicken Sie auf den Link, den wir an Ihren ownChat Account gesendet haben.');
document.getElementById('external-call-overlay').style.display = 'flex';
// Externe URL in neuem Tab/Fenster öffnen
window.open(result.url, '_blank');
// Polling starten - prüft regelmäßig ob der Callback aufgerufen wurde
_externalCallPollingTimer = setInterval(function() {
$.ajax({
url: '@Url.Action("CheckExternalCallStatus", "Main")',
data: { callId: _externalCallId },
type: 'GET',
success: function(pollResult) {
if (pollResult && pollResult.completed) {
stopExternalCallPolling();
onExternalCallCompleted(pollResult.data);
} else if (pollResult && pollResult.statusMessage) {
$('#external-call-status-text').text(pollResult.statusMessage);
}
},
error: function() {
// Bei Fehler weiter pollen
}
});
}, 3000);
} else {
// Fehler beim Ermitteln der URL
var errorMsg = (result && result.message) ? result.message : 'Die externe URL konnte nicht ermittelt werden.';
showAlertMessageBox(errorMsg);
_externalCallId = null;
}
},
error: function() {
showAlertMessageBox('Fehler bei der Kommunikation mit dem Server.');
_externalCallId = null;
}
});
}
function cancelExternalCall() {
// callId merken BEVOR stopExternalCallPolling() sie auf null setzt
var callIdToCancel = _externalCallId;
stopExternalCallPolling();
// Server informieren, dass abgebrochen wurde
if (callIdToCancel) {
$.ajax({
url: '@Url.Action("CancelExternalCall", "Main")',
data: { callId: callIdToCancel },
type: 'POST'
});
}
}
function stopExternalCallPolling() {
if (_externalCallPollingTimer) {
clearInterval(_externalCallPollingTimer);
_externalCallPollingTimer = null;
}
_externalCallId = null;
document.getElementById('external-call-overlay').style.display = 'none';
}
function onExternalCallCompleted(data) {
var $target;
var targetIndex;
// Prüfen ob die Tab-Ansicht sichtbar ist (mehrere Dokutypes vorhanden)
var $activeTabTextarea = $('#dokufelderTab .tab-pane.active textarea');
if ($activeTabTextarea.length > 0) {
// Das aktive Tab-Textarea verwenden; Index aus der ID auslesen (sb-doku-textarea-{i})
$target = $activeTabTextarea;
targetIndex = parseInt($target.attr('id').replace('sb-doku-textarea-', ''), 10);
} else {
// Fallback: Einzelfeld Dokumentation6
$target = $('#sb-doku-textarea-5');
targetIndex = 5;
}
if ($target.length > 0) {
$target.val(data);
window.updateDokuText('sb', data, targetIndex);
}
}
</script>
@using(Html.BeginForm("ResetSingleBookingForm", "Main", FormMethod.Post, new { Id = "reset-single-booking-form-form" })) { }
<div class="row">
@@ -580,29 +684,47 @@
</div>
}
<div class="w-100 mt-2">
<button type="button" class="btn btn-primary float-right ml-1" id="create-button" onclick="submitSingleBookingForm()">@Model.SubmitButtonValue</button>
<div class="w-100 mt-2">
<button type="button" class="btn btn-primary float-right ml-1" id="create-button" onclick="submitSingleBookingForm()">@Model.SubmitButtonValue</button>
@if(Model.IsInEditingMode)
@if (Model.IsInEditingMode)
{
using (Html.BeginForm("ResetEditingMode", "Main", FormMethod.Post))
{
using(Html.BeginForm("ResetEditingMode", "Main", FormMethod.Post))
{
<button type="submit" class="btn btn-primary float-right" onclick="showSpinner()">Abbrechen</button>
}
<button type="submit" class="btn btn-primary float-right" onclick="showSpinner()">Abbrechen</button>
}
else if(Model.IsInTransferMode)
}
else if (Model.IsInTransferMode)
{
using (Html.BeginForm("ResetTransferMode", "Main", FormMethod.Post))
{
using(Html.BeginForm("ResetTransferMode", "Main", FormMethod.Post))
{
<button type="submit" class="btn btn-primary float-right" onclick="showSpinner()">Abbrechen</button>
}
<button type="submit" class="btn btn-primary float-right" onclick="showSpinner()">Abbrechen</button>
}
else
{
<button type="button" class="btn btn-primary float-right" id="reset-button" onclick="resetSingleBookingForm();">Abbrechen</button>
}
@Html.Partial("RestoreServiceRecordInputsPartial", Model)
}
else
{
<button type="button" class="btn btn-primary float-right" id="reset-button" onclick="resetSingleBookingForm();">Abbrechen</button>
}
@if (Model.HasRightToUseAiVoice)
{
<button type="button" class="btn btn-primary float-right mr-1" id="aivoice-button" onclick="startExternalCall();" title="KI-Spracheingabe & Transkription" style="padding: 0 0.6em; overflow: hidden; line-height: 0;"><img src="@Url.Content("~/Content/images/Icon_Mikro_Weiss.png")" alt="KI-Spracheingabe & Transkription" style="height:2.25em; display: block;" /></button>
}
@Html.Partial("RestoreServiceRecordInputsPartial", Model)
</div>
@* Waiting-Overlay für externen Aufruf *@
<div id="external-call-overlay" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:9999; justify-content:center; align-items:center;">
<div style="background:white; border-radius:8px; padding:30px; text-align:center; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
<div class="mb-3">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</div>
<p class="mb-3" id="external-call-status-text">Warte auf Rückmeldung vom externen System...</p>
<button type="button" class="btn btn-danger" onclick="cancelExternalCall();">Abbrechen</button>
</div>
</div>
}
</div>

View File

@@ -267,7 +267,7 @@
var hasNoOwnEntries = !Model.HasRightToProvideEmployeeSignatureForOthers && result.Items.All(item => !item.EmployeeOid.Equals(Model.LoggedInEmployee.EmployeeOid));
var disabledString = hasNoOwnEntries ? "disabled" : string.Empty;
var customerDisabledString = result.FristAbgelaufen ? "disabled" : string.Empty;
var fristAbgelaufen = result.FristAbgelaufen ? "true" : "false";
}
<div class="row">
@@ -299,7 +299,7 @@
<span class="fas fa-trash"></span>
</button>
}
<button class="btn btn-bewo-customers" @customerDisabledString type="button" onclick="loadConfirmationReceiptSignatures('@cardBodyId_ssp', '@result.Identifier', false, @result.HasCustomerSignature.ToString().ToLower(), @canCustomerSign.ToString().ToLower(), @result.CustomerOid)">
<button class="btn btn-bewo-customers" type="button" onclick="if(@fristAbgelaufen){showAlertMessageBoxWithoutCallback('Unterschreiben nicht möglich.', 'Die Frist, in der neue Einträge unterschrieben werden müssen, ist leider abgelaufen.');}else{loadConfirmationReceiptSignatures('@cardBodyId_ssp', '@result.Identifier', false, @result.HasCustomerSignature.ToString().ToLower(), @canCustomerSign.ToString().ToLower(), @result.CustomerOid);}">
<span class="fas fa-signature mr-0 pr-0"></span>
@if(customerSignatureState != SignatureState.All)
{

View File

@@ -55,7 +55,7 @@
<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.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/view-scripts/main.js?v=2.7")"></script>
<script type="text/javascript" src="@Scripts.Url("~/Scripts/ownSoft-Scripts/signature.js?v=1.5")"></script>
<link type="text/css" rel="stylesheet" href="@Url.Content("~/Scripts/bootstrap/bootstrap4-modal-fullscreen.min.css")" />
@@ -361,7 +361,6 @@
<span class="logoff-countdown m-0 p-0" id="countdown" onclick="countdownClick()">&nbsp;</span>
</p>
</a>
<span class="logoff-countdown m-0 p-0">Version 3.30</span>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive">
<span class="navbar-toggler-icon"></span>
@@ -398,31 +397,31 @@
</li>
}
}
@if(Model.IsAllowedToSeePersons)
@if (Model.IsAllowedToSeePersons)
{
using(Html.BeginForm("RedirectToPersons", "Main", FormMethod.Post))
{
<li class="nav-item">
<button class="btn btn-link text-light no-underline" onclick="showSpinner()">
<i class="fas fa-user text-bewo-person"></i>
Personen
</button>
</li>
}
using (Html.BeginForm("RedirectToPersons", "Main", FormMethod.Post))
{
<li class="nav-item">
<button class="btn btn-link text-light no-underline" onclick="showSpinner()">
<i class="fas fa-user text-bewo-person"></i>
Personen
</button>
</li>
}
}
@if(Model.HasRightToViewOwnEmployee)
@if (Model.HasRightToViewOwnEmployee)
{
using(Html.BeginForm("RedirectToEmployees", "Main", FormMethod.Post))
{
<li class="nav-item">
<button class="btn btn-link text-light no-underline" onclick="showSpinner()">
<i class="fas fa-user text-bewo-employee"></i>
Mitarbeiter
</button>
</li>
}
using (Html.BeginForm("RedirectToEmployees", "Main", FormMethod.Post))
{
<li class="nav-item">
<button class="btn btn-link text-light no-underline" onclick="showSpinner()">
<i class="fas fa-user text-bewo-employee"></i>
Mitarbeiter
</button>
</li>
}
}
@if (Model.IsAllowedToSeeCustomers)
@@ -514,6 +513,10 @@
</button>
}
</li>
<li class="nav-item">
<span class="logoff-countdown m-0 p-0">Version 3.31</span>
</li>
</ul>
</div>
</div>