AI Voice zum Mok hinzugefügt

This commit is contained in:
2026-03-30 00:07:52 +02:00
parent acdb064f4a
commit fe7f2da2c9
7 changed files with 314 additions and 22 deletions

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;
@@ -3557,6 +3566,131 @@ 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.ResultUrl;
if (!string.IsNullOrEmpty(callId) && !string.IsNullOrEmpty(avd.Token))
{
_externalCallTokens[callId] = ts.MapAiVoiceDataDC(avd);
}
//var externalBaseUrl = "https://example.com/external-service";
//var externalUrl = externalBaseUrl
// + "?callbackUrl=" + Uri.EscapeDataString(callbackUrl)
// + "&callId=" + Uri.EscapeDataString(callId);
}
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;
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;
avddc = ts.MapAiVoiceDataDC(avd);
_externalCallTokens[callId] = avddc;
//_externalCallStatusMessages.TryGetValue(callId, out statusMessage);
}
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,94 @@
}
</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) {
// Hier kann die Rückmeldung vom externen System verarbeitet werden
// z.B. Daten in ein Formularfeld schreiben
console.log('Externer Aufruf abgeschlossen. Daten:', data);
}
</script>
@using(Html.BeginForm("ResetSingleBookingForm", "Main", FormMethod.Post, new { Id = "reset-single-booking-form-form" })) { }
<div class="row">
@@ -580,29 +668,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();">AI Voice</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

@@ -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")" />