AiVoiceMode ("direct" und "push") eingefügt

This commit is contained in:
2026-04-22 14:52:39 +02:00
parent abd8c0b12e
commit 4e06659a31
9 changed files with 633 additions and 55 deletions

View File

@@ -0,0 +1,284 @@
<localView:BeWoView
x:Class="BeWo.View.Detail.Report.DatenbereinigungView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Microsoft_Windows_Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Classic"
xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core"
xmlns:dxe="http://schemas.devexpress.com/winfx/2008/xaml/editors"
xmlns:dxg="http://schemas.devexpress.com/winfx/2008/xaml/grid"
xmlns:localView="clr-namespace:BeWo.View"
xmlns:localViewDetail="clr-namespace:BeWo.View.Detail.Accounting"
xmlns:localViewModel="clr-namespace:BeWo.ViewModel"
xmlns:markup="clr-namespace:BeWo.MultiLanguage.Markup"
xmlns:proxy="clr-namespace:BeWo.ServiceProxy"
xmlns:r="clr-namespace:BeWo.Security"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:uc="clr-namespace:BeWo.Controls;assembly=BeWo.Controls"
xmlns:val="clr-namespace:BeWo.Validation"
Width="Auto"
Height="Auto"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Focusable="True">
<Grid>
<GroupBox
x:Name="groupbox_editArea"
Grid.Row="0"
Header="{markup:Translate Datenbereinigung}"
Style="{DynamicResource ObjectEditGroupBox}">
<Grid Margin="0,3,0,0" Grid.IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Orientation="Horizontal">
<Label Margin="3">Anzahl Jahre ungenutzt:</Label>
<dxe:SpinEdit
x:Name="editJahre"
Grid.Row="1"
Height="20"
Width="60"
Margin="3"
MinValue="1"
MaxValue="99"
IsFloatValue="False"
VerticalContentAlignment="Center"/>
<RadioButton
x:Name="radioCustomer"
Margin="5,7,5,0"
Content="{markup:Translate Klienten}"
IsChecked="True" />
<RadioButton
x:Name="radioEmployee"
Margin="5,7,5,0"
Content="{markup:Translate Mitarbeiter}"
IsChecked="False" />
<RadioButton
x:Name="radioPersonen"
Margin="5,7,5,0"
Content="{markup:Translate Personen}"
IsChecked="False" />
<Button
x:Name="button_reload"
Grid.Column="1"
Width="80"
Height="24"
Margin="3"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Click="ReloadButton_OnClick"
Content="Aktualisieren" />
</StackPanel>
<dxg:GridControl
x:Name="datagrid_inactiveData"
Grid.Row="1"
Grid.ColumnSpan="4"
Margin="3"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<dxg:GridControl.Columns>
<dxg:GridColumn
x:Name="ColumnChecked"
MinWidth="30"
FieldName="IsChecked"
Header=" " />
<dxg:GridColumn
x:Name="ColumnButtons"
Width="50"
AllowEditing="False"
FieldName="ReportLink"
Header=" ">
<dxg:GridColumn.DisplayTemplate>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<Button
x:Name="btn_delete"
Width="22"
Height="22"
Margin="1"
Padding="0"
VerticalAlignment="Center"
Click="btn_delete_Click"
Content="r"
FontFamily="Webdings"
FontSize="13"
Foreground="#FF0000"
ToolTip="ToolTipDelete" />
<Button
x:Name="btn_anonymisieren"
Width="22"
Height="22"
Margin="1"
Padding="0"
VerticalAlignment="Center"
Content="?"
FontSize="14"
Foreground="#FFFFFF"
Click="btn_anonymisieren_Click"
ToolTip="{markup:Translate ToolTipAnonymisieren}" />
</StackPanel>
</ControlTemplate>
</dxg:GridColumn.DisplayTemplate>
</dxg:GridColumn>
<dxg:GridColumn
Width="150"
AllowEditing="False"
FieldName="LastName"
Header="Nachname"
ReadOnly="True" />
<dxg:GridColumn
Width="100"
AllowEditing="False"
FieldName="FirstName"
Header="Vorname"
ReadOnly="True" />
<dxg:GridColumn
Width="100"
AllowEditing="False"
FieldName="DateOfBirth"
EditSettings="{dxe:DateSettings DisplayFormat=d}"
Header="Geburtsdatum"
ReadOnly="True"
SortFieldName="AccountingPeriodStart" />
<dxg:GridColumn
AllowEditing="False"
FieldName="Street"
Header="Straße"
ReadOnly="True"
SortOrder="Ascending" />
<dxg:GridColumn
MinWidth="70"
AllowEditing="False"
FieldName="PostalCode"
Header="PLZ"
ReadOnly="True" />
<dxg:GridColumn
AllowEditing="False"
FieldName="Town"
Header="Ort"
ReadOnly="True" />
<!--<dxg:GridColumn FieldName="AccountingPeriodString" Header="Abrechnungszeitraum" />-->
<dxg:GridColumn
MinWidth="70"
AllowEditing="False"
FieldName="LastUsedOn"
Header="Zuletzt verwendet am"
ReadOnly="True">
</dxg:GridColumn>
<dxg:GridColumn
AllowEditing="False"
FieldName="Notice"
Header="Bemerkung"
ReadOnly="True" />
</dxg:GridControl.Columns>
<dxg:GridControl.View>
<dxg:TableView
x:Name="gridView"
AllowBestFit="True"
BestFitMode="Smart"
Loaded="gridView_Loaded"
NavigationStyle="Cell"
ScrollingMode="Smart"
ShowGroupPanel="False"
SnapsToDevicePixels="True" />
</dxg:GridControl.View>
</dxg:GridControl>
<Grid
Grid.Row="1"
Grid.ColumnSpan="4"
Margin="5,2,5,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Label
Margin="10,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{Binding FilterResultAmountSum, Converter={StaticResource StringFormatConverter}, ConverterParameter='{}Summe aller Rechnungen: {0:c}'}"
FontSize="14"
Foreground="{StaticResource GridPrintMenuLightBrush}" />
<Label
Margin="0,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content=" | "
FontSize="14"
Foreground="{StaticResource GridPrintMenuLightBrush}" />
<Label
x:Name="lblAmoutNotPaid"
Margin="0,0,0,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{Binding FilterResultAmountNotPaidSum, Converter={StaticResource StringFormatConverter}, ConverterParameter='{}Summe der noch offenen Rechnungen: {0:c}'}"
FontSize="14"
Foreground="{StaticResource GridPrintMenuLightBrush}" />
</StackPanel>
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal">
<Button
Name="button_checkAllLeistungen"
Margin="10,3,0,0"
HorizontalAlignment="Left"
Click="button_checkAll_Click">
Alle auswählen
</Button>
<Button
Name="button_uncheckAllLeistungen"
Margin="10,3,0,0"
HorizontalAlignment="Left"
Click="button_uncheckAll_Click">
Alle abwählen
</Button>
<Button
Name="button_deleteAll"
Margin="20,3,0,0"
HorizontalAlignment="Left"
Click="button_deleteAll_Click">
Ausgewählte löschen
</Button>
<Button
Name="button_anonymisierenAll"
Margin="10,3,0,0"
HorizontalAlignment="Left"
Click="button_anonymisierenAll_Click">
Ausgewählte anonymisieren
</Button>
</StackPanel>
</Grid>
</GroupBox>
</Grid>
</localView:BeWoView>

View File

@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Policy;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Threading;
using BeWo.Core;
using BeWo.ServiceProxy;
using BeWo.View.Windows;
using BeWo.ViewModel;
using BeWo.ViewModel.ListViewModel;
using BS.Shared;
using BS.Shared.Core;
using BS.Shared.DataContracts;
using BS.Shared.DataContracts.Compact;
using BS.Shared.Extensions;
using BS.Shared.Translation;
using DevExpress.Xpf.Editors;
using DevExpress.Xpf.Grid;
using Microsoft.Win32;
using Newtonsoft.Json;
namespace BeWo.View.Detail.Report
{
public partial class DatenbereinigungView : BeWoView
{
public DatenbereinigungView()
{
this.InitializeComponent();
editJahre.Value = 10;
button_reload.Focus();
CheckRights();
}
private void CheckRights()
{
if (!BeWoApp.AppSettings.AllowStorno)
{
ColumnButtons.MinWidth = 67;
}
}
private void RefreshList()
{
DateTime? start = null;
DateTime? end = null;
//bool filterByInvoiceDate = radioFilterByInvoiceDate.IsChecked.Value;
var tid = TableID.Customer;
if (radioEmployee.IsChecked == true) {
tid = TableID.Employee;
} else if (radioPersonen.IsChecked == true) {
tid = TableID.Person;
}
int anzahlJahre = (int)editJahre.Value;
ServiceFacade.DoOperationsServiceAsync(s => s.GetInactivePersons(anzahlJahre, tid), UpdateList);
}
private void UpdateList(IList<InactivePersonDC> dcList)
{
this.Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
(Action)delegate
{
//ViewModel.UpdateList(dcList);
this.datagrid_inactiveData.ItemsSource = dcList;
this.gridView.BestFitColumns();
});
}
private void ReloadButton_OnClick(object sender, RoutedEventArgs e)
{
RefreshList();
}
private void btn_delete_Click(object sender, RoutedEventArgs e)
{
var vm = this.datagrid_inactiveData.GetCurrentValue<InactivePersonDC>();
if (MessageBox.Show("Sind Sie sicher, dass Sie die gewählte Rechnung löschen möchten?", "Rechnung löschen", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes)
{
}
}
private void btn_anonymisieren_Click(object sender, RoutedEventArgs e)
{
var vm = this.datagrid_inactiveData.GetCurrentValue<InactivePersonDC>();
if (MessageBox.Show("Sind Sie sicher, dass Sie die gewählte Rechnung löschen möchten?", "Rechnung löschen", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes)
{
}
}
private void gridView_Loaded(object sender, RoutedEventArgs e)
{
this.gridView.BestFitColumns();
}
private void button_checkAll_Click(object sender, RoutedEventArgs e)
{
var visible = GetVisibleVMs();
foreach (var item in visible)
{
item.IsChecked = true;
}
datagrid_inactiveData.RefreshData();
}
private void button_uncheckAll_Click(object sender, RoutedEventArgs e)
{
var visible = GetVisibleVMs();
foreach (var item in visible)
{
item.IsChecked = false;
}
datagrid_inactiveData.RefreshData();
}
private void button_deleteAll_Click(object sender, RoutedEventArgs e)
{
}
private void button_anonymisierenAll_Click(object sender, RoutedEventArgs e)
{
}
private IList<InactivePersonDC> GetVisibleVMs()
{
var list = new List<InactivePersonDC>();
var items = datagrid_inactiveData.ItemsSource as IList<InactivePersonDC>;
if (items != null)
{
for (int i = 0; i < items.Count; i++)
{
if (datagrid_inactiveData.IsRowVisible(i))
{
var vm = datagrid_inactiveData.GetRow(i) as InactivePersonDC;
if (vm != null)
{
list.Add(vm);
}
}
}
}
return list;
}
}
}

View File

@@ -383,6 +383,8 @@ namespace BeWoPlanerMobil.Controllers
Model.SelectedZeitraum = new NullableDateTimeSpan(Model.LastSelectedServiceRecordMonth.Date.GetFirstOfMonth(), Model.LastSelectedServiceRecordMonth.Date.GetLastOfMonth()); 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); CalculateSelectedDayCount(Model.LastSelectedServiceRecordTimeIntervalDays, Model.LastSelectedServiceRecordMonth, Model.SelectedZeitraum?.StartDate, Model.SelectedZeitraum?.EndDate, null);
var customerFilter = MobileUserSettingsUtils.GetSettingValueAsEnum(userSettings, SettingsKeys.CustomerFilterInZeiterfassung, CustomerFilterEnum.MyCustomer); var customerFilter = MobileUserSettingsUtils.GetSettingValueAsEnum(userSettings, SettingsKeys.CustomerFilterInZeiterfassung, CustomerFilterEnum.MyCustomer);
@@ -3563,35 +3565,65 @@ namespace BeWoPlanerMobil.Controllers
return View("Main", Model); 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> /// <summary>
/// Ermittelt die externe URL, die beim Klick auf den Test-Button aufgerufen werden soll. /// Ermittelt die externe URL, die beim Klick auf den AI-Voice-Button aufgerufen werden soll.
/// Die callbackUrl wird als Parameter mitgegeben, damit das externe System sie verwenden kann. /// Die callId wird server-seitig als GUID erzeugt (kryptographisch sicher) und zusammen
/// mit der URL an den Client zurückgegeben.
/// </summary> /// </summary>
[HttpGet] [HttpGet]
[Authorize] [Authorize]
public JsonResult GetExternalCallUrl(string callId, string callbackUrl) public JsonResult GetExternalCallUrl()
{ {
try 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 = ""; var externalUrl = "";
if (Mandator?.OwnChatCode != null) if (Mandator?.OwnChatCode != null)
{ {
var ts = new TranscriptionService(); 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); _externalCallTokens[callId] = ts.MapAiVoiceDataDC(avd);
} }
Log.Info($"GetExternalCallUrl: {callId}. Url = {externalUrl}, Transkription startet!"); 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 else
{ {
@@ -3646,12 +3678,15 @@ namespace BeWoPlanerMobil.Controllers
/// </summary> /// </summary>
[HttpGet] [HttpGet]
[Authorize] [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)) if(!string.IsNullOrEmpty(callId) && _externalCallResults.TryRemove(callId, out var data))
{ {
_externalCallStatusMessages.TryRemove(callId, out _); _externalCallStatusMessages.TryRemove(callId, out _);
_externalCallTokens.TryRemove(callId, out _); _externalCallTokens.TryRemove(callId, out _);
Session.Remove("ExternalCallId");
return Json(new { completed = true, data }, JsonRequestBehavior.AllowGet); return Json(new { completed = true, data }, JsonRequestBehavior.AllowGet);
} }
@@ -3668,10 +3703,14 @@ namespace BeWoPlanerMobil.Controllers
statusMessage = avd.StatusMessage; statusMessage = avd.StatusMessage;
if (!String.IsNullOrEmpty(statusMessage) && statusMessage.Contains("klicken Sie auf den Link")) if (!String.IsNullOrEmpty(avd.OwnChatUrl)) // Wenn nicht null, dann wurde die Spracheingabe über direkt Link gestartet
{ {
statusMessage = "Sollte die Aufnahme nicht starten, prüfen Sie bitte, ob ownChat korrekt installiert ist."; 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)) if (!String.IsNullOrEmpty(avd.TranskriptionText))
{ {
statusMessage = "Transkription erfolgreich durchgeführt"; statusMessage = "Transkription erfolgreich durchgeführt";
@@ -3708,13 +3747,15 @@ namespace BeWoPlanerMobil.Controllers
/// </summary> /// </summary>
[HttpPost] [HttpPost]
[Authorize] [Authorize]
public ActionResult CancelExternalCall(string callId) public ActionResult CancelExternalCall()
{ {
var callId = Session["ExternalCallId"] as string;
if(!string.IsNullOrEmpty(callId)) if(!string.IsNullOrEmpty(callId))
{ {
_externalCallResults.TryRemove(callId, out _); _externalCallResults.TryRemove(callId, out _);
_externalCallStatusMessages.TryRemove(callId, out _); _externalCallStatusMessages.TryRemove(callId, out _);
_externalCallTokens.TryRemove(callId, out _); _externalCallTokens.TryRemove(callId, out _);
Session.Remove("ExternalCallId");
} }
return new HttpStatusCodeResult(200); return new HttpStatusCodeResult(200);

View File

@@ -9,6 +9,8 @@ namespace BeWoPlanerMobil.Models
[Serializable] [Serializable]
public abstract class BaseSessionModel public abstract class BaseSessionModel
{ {
public int AiVoiceMode { get; set; }
public Dictionary<string, bool> Collapsibles2IsShown { get; set; } = new Dictionary<string, bool>(); public Dictionary<string, bool> Collapsibles2IsShown { get; set; } = new Dictionary<string, bool>();
public virtual void ResetValues() public virtual void ResetValues()

View File

@@ -820,6 +820,8 @@ namespace BeWoPlanerMobil.Models
public bool IsShowingDurationInHours { get; set; } 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 string SelectedDurationUnit => IsShowingDurationInHours ? "Stunden" : "Minuten";
public DateTime LastSelectedServiceRecordMonth { get; set; } public DateTime LastSelectedServiceRecordMonth { get; set; }

View File

@@ -170,37 +170,35 @@
<script type="text/javascript"> <script type="text/javascript">
var _externalCallPollingTimer = null; var _externalCallPollingTimer = null;
var _externalCallId = null;
// Zuletzt verwendeter Modus wird aus der Session initialisiert
var _aiVoiceMode = '@Model.AiVoiceMode';
function startExternalCall() { function startExternalCall() {
// Eindeutige ID für diesen Aufruf generieren // Direkt starten mit dem gespeicherten Modus, keine Moduswahl anzeigen
_externalCallId = 'ext-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); 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({ $.ajax({
url: '@Url.Action("GetExternalCallUrl", "Main")', url: '@Url.Action("GetExternalCallUrl", "Main")',
data: { callId: _externalCallId, callbackUrl: callbackUrl },
type: 'GET', type: 'GET',
success: function(result) { success: function(result) {
if (result && result.success && result.url) { if (result && result.success) {
// 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';
// Custom URL Scheme (ownchat://) per temporärem Anchor-Element öffnen // Custom URL Scheme (ownchat://) per temporärem Anchor-Element öffnen
// zuverlässiger als window.open() für alle Browser inkl. Safari if (result.url && result.url.length > 0) {
var $a = $('<a>').attr('href', result.url).css('display', 'none').appendTo('body'); var $a = $('<a>').attr('href', result.url).css('display', 'none').appendTo('body');
$a[0].click(); $a[0].click();
$a.remove(); $a.remove();
}
// Polling starten - prüft regelmäßig ob der Callback aufgerufen wurde // Polling starten - prüft regelmäßig ob der Callback aufgerufen wurde
_externalCallPollingTimer = setInterval(function() { _externalCallPollingTimer = setInterval(function() {
$.ajax({ $.ajax({
url: '@Url.Action("CheckExternalCallStatus", "Main")', url: '@Url.Action("CheckExternalCallStatus", "Main")',
data: { callId: _externalCallId },
type: 'GET', type: 'GET',
success: function(pollResult) { success: function(pollResult) {
if (pollResult && pollResult.completed) { if (pollResult && pollResult.completed) {
@@ -216,31 +214,53 @@
}); });
}, 3000); }, 3000);
} else { } 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.'; var errorMsg = (result && result.message) ? result.message : 'Die externe URL konnte nicht ermittelt werden.';
showAlertMessageBox(errorMsg); showAlertMessageBox(errorMsg);
_externalCallId = null;
} }
}, },
error: function() { error: function() {
document.getElementById('external-call-overlay').style.display = 'none';
showAlertMessageBox('Fehler bei der Kommunikation mit dem Server.'); 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() { function cancelExternalCall() {
// callId merken BEVOR stopExternalCallPolling() sie auf null setzt
var callIdToCancel = _externalCallId;
stopExternalCallPolling(); stopExternalCallPolling();
// Server informieren, dass abgebrochen wurde // Server informieren, dass abgebrochen wurde callId liegt serverseitig in der Session
if (callIdToCancel) { $.ajax({
$.ajax({ url: '@Url.Action("CancelExternalCall", "Main")',
url: '@Url.Action("CancelExternalCall", "Main")', type: 'POST'
data: { callId: callIdToCancel }, });
type: 'POST'
});
}
} }
function stopExternalCallPolling() { function stopExternalCallPolling() {
@@ -248,7 +268,6 @@
clearInterval(_externalCallPollingTimer); clearInterval(_externalCallPollingTimer);
_externalCallPollingTimer = null; _externalCallPollingTimer = null;
} }
_externalCallId = null;
document.getElementById('external-call-overlay').style.display = 'none'; document.getElementById('external-call-overlay').style.display = 'none';
} }
@@ -719,16 +738,46 @@
@Html.Partial("RestoreServiceRecordInputsPartial", Model) @Html.Partial("RestoreServiceRecordInputsPartial", Model)
</div> </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 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"> <div class="mb-3">
<i class="fa fa-spinner fa-spin fa-2x"></i> <i class="fa fa-spinner fa-spin fa-2x"></i>
</div> </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> <button type="button" class="btn btn-danger" onclick="cancelExternalCall();">Abbrechen</button>
</div> </div>
</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> </div>
@* /Einzelbuchungsformular *@ @* /Einzelbuchungsformular *@

View File

@@ -28,24 +28,22 @@ namespace BeWo.Service.AI
avd = new AiVoiceData(); avd = new AiVoiceData();
avd.Token = "Test"; avd.Token = "Test";
avd.StatusMessage = "1"; avd.StatusMessage = "1";
avd.ResultUrl = "https://example.com/"; avd.OwnChatUrl = null; // "ownchat://voice/?token=test";
return avd; return avd;
#endif #endif
avd = GetTranskriptionToken(tenant, sprache); avd = GetTranskriptionToken(tenant, sprache);
if (!String.IsNullOrEmpty(avd.Token)) if (!String.IsNullOrEmpty(avd.Token))
{ {
avd.OwnChatUrl = String.Format("ownchat://voice/?token={0}", avd.Token); avd.OwnChatUrl = String.Format("ownchat://voice/?token={0}", avd.Token);
if (sendOwnChatMessage) if (sendOwnChatMessage)
{ {
var msg = String.Format("Bitte klicken Sie auf folgenden Link um die Aufnahme zu starten:\n\n{0}", avd.OwnChatUrl); var msg = String.Format("Bitte klicken Sie auf folgenden Link um die Aufnahme zu starten:\n\n{0}", avd.OwnChatUrl);
OwnChatHelper.SendOwnChatMessage(msg, UserRightHelper.GetLoggedInUser().Employee); OwnChatHelper.SendOwnChatMessage(msg, UserRightHelper.GetLoggedInUser().Employee);
} }
} }
return avd; return avd;

View File

@@ -144,5 +144,6 @@ namespace BS.Shared.Core
/// Aktiviert AI Einstellungen (Modell, ...) /// Aktiviert AI Einstellungen (Modell, ...)
/// </summary> /// </summary>
public const string ModuleAiSettingsEnabled = "ModuleAiSettingsEnabled"; public const string ModuleAiSettingsEnabled = "ModuleAiSettingsEnabled";
public const string AiVoiceMode = "AiVoiceMode"; // 0 = direct; 1 = Link zu ownChat senden
} }
} }

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using BS.Shared.Core;
using BS.Shared.Extensions;
using BS.Shared.Interface.Abstract;
namespace BS.Shared.DataContracts
{
[DataContract]
public class InactivePersonDC : IDataContract
{
[DataMember] public string FirstName { get; set; }
[DataMember] public string LastName { get; set; }
[DataMember] public DateTime? DateOfBirth { get; set; }
[DataMember] public int PersonType { get; set; }
[DataMember] public string Street { get; set; }
[DataMember] public string PostalCode { get; set; }
[DataMember] public string Town { get; set; }
[DataMember] public DateTime? LastUsedOn { get; set; }
[DataMember] public string Notice { get; set; }
[DataMember] public long? PersonOid { get; set; }
[DataMember] public long? ObjectOid { get; set; }
[DataMember] public long? ObjectTid { get; set; }
[DataMember] public bool IsChecked { get; set; }
}
}