Merge branch 'master' of ssh://float.ownsoft.de/git/beyondSoft/BeWo
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
using BeWo.Data.Access;
|
||||
using Antlr.Runtime.Tree;
|
||||
using BeWo.Data.Access;
|
||||
using BeWo.Data.Entities;
|
||||
using BeWo.Service.AI;
|
||||
using BeWo.Service.DCEntityMapper;
|
||||
using BeWo.Service.Plugins;
|
||||
using BeWo.View.Navigation.Filter;
|
||||
@@ -32,7 +34,6 @@ 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
|
||||
@@ -382,6 +383,8 @@ namespace BeWoPlanerMobil.Controllers
|
||||
Model.SelectedZeitraum = new NullableDateTimeSpan(Model.LastSelectedServiceRecordMonth.Date.GetFirstOfMonth(), Model.LastSelectedServiceRecordMonth.Date.GetLastOfMonth());
|
||||
}
|
||||
|
||||
Model.AiVoiceMode = UserSettingsUtils.GetSettingValueAsInteger(userSettings, SettingsKeys.AiVoiceMode);
|
||||
|
||||
CalculateSelectedDayCount(Model.LastSelectedServiceRecordTimeIntervalDays, Model.LastSelectedServiceRecordMonth, Model.SelectedZeitraum?.StartDate, Model.SelectedZeitraum?.EndDate, null);
|
||||
|
||||
var customerFilter = MobileUserSettingsUtils.GetSettingValueAsEnum(userSettings, SettingsKeys.CustomerFilterInZeiterfassung, CustomerFilterEnum.MyCustomer);
|
||||
@@ -3562,35 +3565,65 @@ namespace BeWoPlanerMobil.Controllers
|
||||
return View("Main", Model);
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public ActionResult SetAiVoiceMode(string mode)
|
||||
{
|
||||
SkipModelInitialization = true;
|
||||
Model.AiVoiceMode = mode == "1" ? 1 : 0;
|
||||
|
||||
UpdateUserSettingsWithoutReload(SettingsKeys.AiVoiceMode, Model.AiVoiceMode.ToString());
|
||||
|
||||
return new HttpStatusCodeResult(200);
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Ermittelt die externe URL, die beim Klick auf den AI-Voice-Button aufgerufen werden soll.
|
||||
/// Die callId wird server-seitig als GUID erzeugt (kryptographisch sicher) und zusammen
|
||||
/// mit der URL an den Client zurückgegeben.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public JsonResult GetExternalCallUrl(string callId, string callbackUrl)
|
||||
public JsonResult GetExternalCallUrl()
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
SkipModelInitialization = true;
|
||||
// CallId server-seitig erzeugen – Guid.NewGuid() ist kryptographisch sicher (128-Bit)
|
||||
// In der Session speichern – der Browser bekommt sie nie zu sehen
|
||||
var callId = Guid.NewGuid().ToString("N");
|
||||
Session["ExternalCallId"] = callId;
|
||||
|
||||
// CallbackUrl server-seitig zusammenbauen
|
||||
var callbackUrl = Url.Action("ExternalCallCallback", "Main", new { callId }, Request.Url.Scheme);
|
||||
|
||||
var externalUrl = "";
|
||||
|
||||
if (Mandator?.OwnChatCode != null)
|
||||
{
|
||||
var ts = new TranscriptionService();
|
||||
var avd = ts.InitTranskription(MobileSessionFacade.Tenant, false);
|
||||
var sendOwnChatMessage = Model.AiVoiceMode == 0 ? false : true;
|
||||
|
||||
externalUrl = avd.OwnChatUrl;
|
||||
var avd = ts.InitTranskription(MobileSessionFacade.Tenant, sendOwnChatMessage);
|
||||
|
||||
if (!string.IsNullOrEmpty(callId) && !string.IsNullOrEmpty(avd.Token))
|
||||
if (!sendOwnChatMessage)
|
||||
{
|
||||
externalUrl = avd.OwnChatUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
avd.OwnChatUrl = null;
|
||||
}
|
||||
|
||||
|
||||
if (!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
|
||||
{
|
||||
@@ -3645,12 +3678,15 @@ namespace BeWoPlanerMobil.Controllers
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public JsonResult CheckExternalCallStatus(string callId)
|
||||
public JsonResult CheckExternalCallStatus()
|
||||
{
|
||||
var callId = Session["ExternalCallId"] as string;
|
||||
|
||||
if(!string.IsNullOrEmpty(callId) && _externalCallResults.TryRemove(callId, out var data))
|
||||
{
|
||||
_externalCallStatusMessages.TryRemove(callId, out _);
|
||||
_externalCallTokens.TryRemove(callId, out _);
|
||||
Session.Remove("ExternalCallId");
|
||||
return Json(new { completed = true, data }, JsonRequestBehavior.AllowGet);
|
||||
}
|
||||
|
||||
@@ -3666,8 +3702,15 @@ namespace BeWoPlanerMobil.Controllers
|
||||
avd = ts.GetTranskription(avd);
|
||||
|
||||
statusMessage = avd.StatusMessage;
|
||||
|
||||
|
||||
if (!String.IsNullOrEmpty(avd.OwnChatUrl)) // Wenn nicht null, dann wurde die Spracheingabe über direkt Link gestartet
|
||||
{
|
||||
if (!String.IsNullOrEmpty(statusMessage) && statusMessage.Contains("klicken Sie auf den Link"))
|
||||
{
|
||||
statusMessage = "Warte auf Beendigung der Aufzeichnung in ownChat...\r\nHINWEIS: Wenn ownChat auf diesem Gerät nicht installiert ist, ist die Spracheingabe nicht möglich. In diesem Fall brechen Sie den Vorgang bitte ab. Wenn Sie ownChat auf einem anderen Gerät nutzen, können Sie in den Einstellungen auch in den Push-Modus wechseln.";
|
||||
}
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(avd.TranskriptionText))
|
||||
{
|
||||
statusMessage = "Transkription erfolgreich durchgeführt";
|
||||
@@ -3704,13 +3747,15 @@ namespace BeWoPlanerMobil.Controllers
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
public ActionResult CancelExternalCall(string callId)
|
||||
public ActionResult CancelExternalCall()
|
||||
{
|
||||
var callId = Session["ExternalCallId"] as string;
|
||||
if(!string.IsNullOrEmpty(callId))
|
||||
{
|
||||
_externalCallResults.TryRemove(callId, out _);
|
||||
_externalCallStatusMessages.TryRemove(callId, out _);
|
||||
_externalCallTokens.TryRemove(callId, out _);
|
||||
Session.Remove("ExternalCallId");
|
||||
}
|
||||
|
||||
return new HttpStatusCodeResult(200);
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace BeWoPlanerMobil.Models
|
||||
[Serializable]
|
||||
public abstract class BaseSessionModel
|
||||
{
|
||||
public int AiVoiceMode { get; set; }
|
||||
|
||||
public Dictionary<string, bool> Collapsibles2IsShown { get; set; } = new Dictionary<string, bool>();
|
||||
|
||||
public virtual void ResetValues()
|
||||
|
||||
@@ -820,6 +820,8 @@ namespace BeWoPlanerMobil.Models
|
||||
|
||||
public bool IsShowingDurationInHours { get; set; }
|
||||
|
||||
public int AiVoiceMode { get { return MainSessionModel.AiVoiceMode; } set { MainSessionModel.AiVoiceMode = value; } } // 0 = direct, 1 = Link senden
|
||||
|
||||
public string SelectedDurationUnit => IsShowingDurationInHours ? "Stunden" : "Minuten";
|
||||
|
||||
public DateTime LastSelectedServiceRecordMonth { get; set; }
|
||||
|
||||
@@ -170,34 +170,35 @@
|
||||
|
||||
<script type="text/javascript">
|
||||
var _externalCallPollingTimer = null;
|
||||
var _externalCallId = null;
|
||||
|
||||
// Zuletzt verwendeter Modus – wird aus der Session initialisiert
|
||||
var _aiVoiceMode = '@Model.AiVoiceMode';
|
||||
|
||||
function startExternalCall() {
|
||||
// Eindeutige ID für diesen Aufruf generieren
|
||||
_externalCallId = 'ext-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
// Direkt starten mit dem gespeicherten Modus, keine Moduswahl anzeigen
|
||||
if (_aiVoiceMode === '1') {
|
||||
$('#external-call-status-text').text('Um die Aufnahme zu starten, klicken Sie auf den Link, den wir an Ihren ownChat Account gesendet haben.');
|
||||
}
|
||||
else {
|
||||
$('#external-call-status-text').text('Bitte wechseln Sie zu ownChat um die Aufnahme zu starten.\nHINWEIS: Wenn ownChat auf diesem Gerät nicht installiert ist, ist die Spracheingabe nicht möglich.');
|
||||
}
|
||||
document.getElementById('external-call-overlay').style.display = 'flex';
|
||||
|
||||
// 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');
|
||||
|
||||
if (result && result.success) {
|
||||
// Custom URL Scheme (ownchat://) per temporärem Anchor-Element öffnen –
|
||||
if (result.url && result.url.length > 0) {
|
||||
var $a = $('<a>').attr('href', result.url).css('display', 'none').appendTo('body');
|
||||
$a[0].click();
|
||||
$a.remove();
|
||||
}
|
||||
// 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) {
|
||||
@@ -213,31 +214,53 @@
|
||||
});
|
||||
}, 3000);
|
||||
} else {
|
||||
// Fehler beim Ermitteln der URL
|
||||
document.getElementById('external-call-overlay').style.display = 'none';
|
||||
var errorMsg = (result && result.message) ? result.message : 'Die externe URL konnte nicht ermittelt werden.';
|
||||
showAlertMessageBox(errorMsg);
|
||||
_externalCallId = null;
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
document.getElementById('external-call-overlay').style.display = 'none';
|
||||
showAlertMessageBox('Fehler bei der Kommunikation mit dem Server.');
|
||||
_externalCallId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openAiVoiceSettings() {
|
||||
// Aktuellen Modus im Einstellungs-Overlay vorauswählen
|
||||
$('input[name="aiVoiceMode"][value="' + _aiVoiceMode + '"]').prop('checked', true);
|
||||
document.getElementById('aivoice-settings-overlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeAiVoiceSettings() {
|
||||
document.getElementById('aivoice-settings-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveAiVoiceMode() {
|
||||
var selectedMode = $('input[name="aiVoiceMode"]:checked').val();
|
||||
$.ajax({
|
||||
url: '@Url.Action("SetAiVoiceMode", "Main")',
|
||||
data: { mode: selectedMode },
|
||||
type: 'POST',
|
||||
success: function() {
|
||||
_aiVoiceMode = selectedMode;
|
||||
closeAiVoiceSettings();
|
||||
cancelExternalCall();
|
||||
showMessagePopup('Einstellungen gespeichert', 'Die Änderungen wurden übernommen. Bitte starten Sie die Aufnahme erneut.');
|
||||
},
|
||||
error: function() {
|
||||
showAlertMessageBox('Fehler beim Speichern der Einstellung.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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'
|
||||
});
|
||||
}
|
||||
// Server informieren, dass abgebrochen wurde – callId liegt serverseitig in der Session
|
||||
$.ajax({
|
||||
url: '@Url.Action("CancelExternalCall", "Main")',
|
||||
type: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
function stopExternalCallPolling() {
|
||||
@@ -245,7 +268,6 @@
|
||||
clearInterval(_externalCallPollingTimer);
|
||||
_externalCallPollingTimer = null;
|
||||
}
|
||||
_externalCallId = null;
|
||||
document.getElementById('external-call-overlay').style.display = 'none';
|
||||
}
|
||||
|
||||
@@ -716,16 +738,46 @@
|
||||
@Html.Partial("RestoreServiceRecordInputsPartial", Model)
|
||||
</div>
|
||||
|
||||
@* Waiting-Overlay für externen Aufruf *@
|
||||
@* Warte-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 style="background:white; border-radius:8px; padding:30px; min-width:300px; 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>
|
||||
<p class="mb-3" id="external-call-status-text" style="white-space: pre-line">Bitte warten...</p>
|
||||
<button type="button" class="btn btn-secondary mr-2" onclick="openAiVoiceSettings();">
|
||||
<i class="fa fa-cog mr-1"></i>Einstellungen
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger" onclick="cancelExternalCall();">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Einstellungs-Overlay für Moduswahl *@
|
||||
<div id="aivoice-settings-overlay" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:10000; justify-content:center; align-items:center;">
|
||||
<div style="background:white; border-radius:8px; padding:30px; max-width:480px; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
|
||||
<h6 class="mb-3">Modus für Spracheingabe</h6>
|
||||
<div class="form-group text-left mb-4">
|
||||
<div class="form-check mb-3">
|
||||
<input class="form-check-input" type="radio" name="aiVoiceMode" id="aiVoiceModeDirect" value="0" />
|
||||
<label class="form-check-label font-weight-bold" for="aiVoiceModeDirect">
|
||||
Spracheingabe direkt aus Mobilklient starten
|
||||
</label>
|
||||
<small class="form-text text-muted">Diese Option setzt voraus, dass Mobilklient und ownChat auf gleichem Endgerät genutzt werden</small>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="aiVoiceMode" id="aiVoiceModePush" value="1" />
|
||||
<label class="form-check-label font-weight-bold" for="aiVoiceModePush">
|
||||
Spracheingabe per Push-Nachricht starten
|
||||
</label>
|
||||
<small class="form-text text-muted">Diese Option ermöglicht die Nutzung von Mobilklient und ownChat auf getrennten Endgeräten wie bei der Vollversion</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="button" class="btn btn-secondary mr-2" onclick="closeAiVoiceSettings();">Schließen</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveAiVoiceMode();">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@* /Einzelbuchungsformular *@
|
||||
|
||||
Reference in New Issue
Block a user