diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index bfc1512..bd94f65 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,8 +47,8 @@ ## Services & Dependency Injection - Business logic lives in **DI-registered services** under `Fuchs/Services/` behind interfaces; inject them into `IntranetController` (constructor injection). Do **not** reintroduce static God-classes or pass the whole controller into helpers. - - `IComService` (email/SMS via ProcessWeb Mailer API, attachments sent inline as base64; honors the `Fuchs:Email:OverrideRecipient` dev safety net — see Configuration), `IPdfService` (MigraDoc render), `IInvoiceService`, `IReminderService`, `IReportService` (SQL report engine via `FuchsVisualization`), `IWidgetService`, `IBankingService`, `IMfrClientFactory`. - - Lifetimes: stateless services (`IPdfService`, `IBankingService`, `IMfrClientFactory`) are singletons; request-scoped DB services (`IInvoiceService`, `IReminderService`, `IReportService`, `IWidgetService`, `IComService`) are scoped. Register in `Program.cs`. + - `IComService` (email/SMS via ProcessWeb Mailer API, attachments sent inline as base64; honors the `Fuchs:Email:OverrideRecipient` dev safety net — see Configuration), `IPdfService` (MigraDoc render), `IInvoiceService`, `IReminderService`, `IReportService` (SQL report engine via `FuchsVisualization`), `IWidgetService`, `IBankingService`, `IMfrClientFactory`, `ISystemStatusService` (Admin module diagnostics: config snapshot + DB/Key Vault/blob/MFR connectivity probes + test-email; restricted to `fds_sys` > 4). + - Lifetimes: stateless services (`IPdfService`, `IBankingService`, `IMfrClientFactory`) are singletons; request-scoped DB services (`IInvoiceService`, `IReminderService`, `IReportService`, `IWidgetService`, `IComService`, `ISystemStatusService`) are scoped. Register in `Program.cs`. - `FdsInvoiceData` / `FdsReminderData` are **pure data holders** (parse + properties). Loading, persistence and PDF generation belong in the services — never `Task.Run(...).Wait()` sync-over-async. - Data access stays SQL-first via OCORE helpers (`getSQLDataSet_async`, `setSQLValue_async`) + stored procedures; no EF Core. diff --git a/CLAUDE.md b/CLAUDE.md index c6b1216..9fb8c9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ ## Services & Dependency Injection - Business logic lives in **DI-registered services** under `Fuchs/Services/` behind interfaces, injected into `IntranetController`. Do not reintroduce static God-classes or pass the controller into helpers. -- Services: `IComService` (ProcessWeb Mailer API; honors the `Fuchs:Email:OverrideRecipient` dev safety net — see Configuration), `IPdfService`, `IInvoiceService`, `IReminderService`, `IReportService`, `IWidgetService`, `IBankingService`, `IMfrClientFactory`. Stateless ones are singletons; DB/request-scoped ones are scoped (see `Program.cs`). +- Services: `IComService` (ProcessWeb Mailer API; honors the `Fuchs:Email:OverrideRecipient` dev safety net — see Configuration), `IPdfService`, `IInvoiceService`, `IReminderService`, `IReportService`, `IWidgetService`, `IBankingService`, `IMfrClientFactory`, `ISystemStatusService` (Admin module diagnostics: config snapshot + DB/Key Vault/blob/MFR connectivity probes + test-email; restricted to `fds_sys` > 4). Stateless ones are singletons; DB/request-scoped ones are scoped (see `Program.cs`). - `FdsInvoiceData` / `FdsReminderData` are **pure data holders**; load/persist/render belongs in services. No `Task.Run(...).Wait()` sync-over-async. - Data access is SQL-first via OCORE helpers + stored procedures (no EF Core). diff --git a/CODEX.md b/CODEX.md index 9058e9c..5d6495d 100644 --- a/CODEX.md +++ b/CODEX.md @@ -37,7 +37,7 @@ ## Services & Dependency Injection - Business logic lives in **DI-registered services** under `Fuchs/Services/` behind interfaces, injected into `IntranetController`. Do not reintroduce static God-classes or pass the controller into helpers. -- Services: `IComService` (ProcessWeb Mailer API; honors the `Fuchs:Email:OverrideRecipient` dev safety net — see Configuration), `IPdfService`, `IInvoiceService`, `IReminderService`, `IReportService`, `IWidgetService`, `IBankingService`, `IMfrClientFactory`. Stateless ones are singletons; DB/request-scoped ones are scoped (see `Program.cs`). +- Services: `IComService` (ProcessWeb Mailer API; honors the `Fuchs:Email:OverrideRecipient` dev safety net — see Configuration), `IPdfService`, `IInvoiceService`, `IReminderService`, `IReportService`, `IWidgetService`, `IBankingService`, `IMfrClientFactory`, `ISystemStatusService` (Admin module diagnostics: config snapshot + DB/Key Vault/blob/MFR connectivity probes + test-email; restricted to `fds_sys` > 4). Stateless ones are singletons; DB/request-scoped ones are scoped (see `Program.cs`). - `FdsInvoiceData` / `FdsReminderData` are **pure data holders**; load/persist/render belongs in services. No `Task.Run(...).Wait()` sync-over-async. - Data access is SQL-first via OCORE helpers + stored procedures (no EF Core). diff --git a/DataService_Legacy/7z.dll b/DataService_Legacy/7z.dll deleted file mode 100644 index b32d7bf..0000000 Binary files a/DataService_Legacy/7z.dll and /dev/null differ diff --git a/DataService_Legacy/App.config b/DataService_Legacy/App.config deleted file mode 100644 index eb4f30b..0000000 --- a/DataService_Legacy/App.config +++ /dev/null @@ -1,131 +0,0 @@ - - - - -
- - - - - - - - - - - - - - 15 - - - True - - - system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com - - - 0oT4G3H2 - - - portal.mobilefieldreport.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/DataService_Legacy/Fuchs_DataService.vbproj b/DataService_Legacy/Fuchs_DataService.vbproj deleted file mode 100644 index 3a383f2..0000000 --- a/DataService_Legacy/Fuchs_DataService.vbproj +++ /dev/null @@ -1,186 +0,0 @@ - - - - - Debug - AnyCPU - {7A56E271-A6BE-4C34-A859-DADEBC4C7A54} - Exe - Sub Main - fds - Fuchs_DataService - 512 - Console - v4.8 - true - true - false - publish\ - true - Disk - false - Foreground - 7 - Days - false - false - true - 0 - 1.0.0.%2a - false - true - - - x64 - true - full - true - true - bin\Debug\ - Fuchs_DataService.xml - 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 - false - - - AnyCPU - pdbonly - false - true - true - bin\Release\ - Fuchs_DataService.xml - 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 - - - On - - - Binary - - - Off - - - On - - - - ..\..\..\NugetPackages\Microsoft.Web.Infrastructure.2.0.0\lib\net40\Microsoft.Web.Infrastructure.dll - - - ..\..\..\NugetPackages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll - - - ..\..\..\NugetPackages\Squid-Box.SevenZipSharp.1.6.1.23\lib\net472\SevenZipSharp.dll - - - - - - - - ..\..\..\NugetPackages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll - True - True - - - - - ..\..\..\NugetPackages\Microsoft.AspNet.Razor.3.2.9\lib\net45\System.Web.Razor.dll - - - - - - - - ..\..\..\NugetPackages\Topshelf.4.3.0\lib\net452\Topshelf.dll - - - - - - - - - - - - - - - - - - - - - True - Application.myapp - True - - - True - True - Resources.resx - - - True - Settings.settings - True - - - - - - VbMyResourcesResXFileCodeGenerator - Resources.Designer.vb - My.Resources - Designer - - - - - - MyApplicationCodeGenerator - Application.Designer.vb - - - SettingsSingleFileGenerator - My - Settings.Designer.vb - - - - - - - - {ac8cba60-d786-48fd-a9f0-8b045a7bd505} - OCMS - - - {00c70b53-516d-4d56-ad25-6757094b4335} - MFR_RESTClient - - - - - Always - - - - - False - Microsoft .NET Framework 4.8 %28x86 and x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - - - \ No newline at end of file diff --git a/DataService_Legacy/My Project/Application.Designer.vb b/DataService_Legacy/My Project/Application.Designer.vb deleted file mode 100644 index 88dd01c..0000000 --- a/DataService_Legacy/My Project/Application.Designer.vb +++ /dev/null @@ -1,13 +0,0 @@ -'------------------------------------------------------------------------------ -' -' This code was generated by a tool. -' Runtime Version:4.0.30319.42000 -' -' Changes to this file may cause incorrect behavior and will be lost if -' the code is regenerated. -' -'------------------------------------------------------------------------------ - -Option Strict On -Option Explicit On - diff --git a/DataService_Legacy/My Project/Application.myapp b/DataService_Legacy/My Project/Application.myapp deleted file mode 100644 index e62f1a5..0000000 --- a/DataService_Legacy/My Project/Application.myapp +++ /dev/null @@ -1,10 +0,0 @@ - - - false - false - 0 - true - 0 - 2 - true - diff --git a/DataService_Legacy/My Project/AssemblyInfo.vb b/DataService_Legacy/My Project/AssemblyInfo.vb deleted file mode 100644 index e65a15f..0000000 --- a/DataService_Legacy/My Project/AssemblyInfo.vb +++ /dev/null @@ -1,35 +0,0 @@ -Imports System -Imports System.Reflection -Imports System.Runtime.InteropServices - -' General Information about an assembly is controlled through the following -' set of attributes. Change these attribute values to modify the information -' associated with an assembly. - -' Review the values of the assembly attributes - - - - - - - - - - -'The following GUID is for the ID of the typelib if this project is exposed to COM - - -' Version information for an assembly consists of the following four values: -' -' Major Version -' Minor Version -' Build Number -' Revision -' -' You can specify all the values or you can default the Build and Revision Numbers -' by using the '*' as shown below: -' - - - diff --git a/DataService_Legacy/My Project/Resources.Designer.vb b/DataService_Legacy/My Project/Resources.Designer.vb deleted file mode 100644 index d04821b..0000000 --- a/DataService_Legacy/My Project/Resources.Designer.vb +++ /dev/null @@ -1,63 +0,0 @@ -'------------------------------------------------------------------------------ -' -' This code was generated by a tool. -' Runtime Version:4.0.30319.42000 -' -' Changes to this file may cause incorrect behavior and will be lost if -' the code is regenerated. -' -'------------------------------------------------------------------------------ - -Option Strict On -Option Explicit On - -Imports System - -Namespace My.Resources - - 'This class was auto-generated by the StronglyTypedResourceBuilder - 'class via a tool like ResGen or Visual Studio. - 'To add or remove a member, edit your .ResX file then rerun ResGen - 'with the /str option, or rebuild your VS project. - ''' - ''' A strongly-typed resource class, for looking up localized strings, etc. - ''' - _ - Friend Module Resources - - Private resourceMan As Global.System.Resources.ResourceManager - - Private resourceCulture As Global.System.Globalization.CultureInfo - - ''' - ''' Returns the cached ResourceManager instance used by this class. - ''' - _ - Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager - Get - If Object.ReferenceEquals(resourceMan, Nothing) Then - Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("fds.Resources", GetType(Resources).Assembly) - resourceMan = temp - End If - Return resourceMan - End Get - End Property - - ''' - ''' Overrides the current thread's CurrentUICulture property for all - ''' resource lookups using this strongly typed resource class. - ''' - _ - Friend Property Culture() As Global.System.Globalization.CultureInfo - Get - Return resourceCulture - End Get - Set - resourceCulture = value - End Set - End Property - End Module -End Namespace diff --git a/DataService_Legacy/My Project/Resources.resx b/DataService_Legacy/My Project/Resources.resx deleted file mode 100644 index af7dbeb..0000000 --- a/DataService_Legacy/My Project/Resources.resx +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - \ No newline at end of file diff --git a/DataService_Legacy/My Project/Settings.Designer.vb b/DataService_Legacy/My Project/Settings.Designer.vb deleted file mode 100644 index 694c998..0000000 --- a/DataService_Legacy/My Project/Settings.Designer.vb +++ /dev/null @@ -1,118 +0,0 @@ -'------------------------------------------------------------------------------ -' -' This code was generated by a tool. -' Runtime Version:4.0.30319.42000 -' -' Changes to this file may cause incorrect behavior and will be lost if -' the code is regenerated. -' -'------------------------------------------------------------------------------ - -Option Strict On -Option Explicit On - - -Namespace My - - _ - Partial Friend NotInheritable Class MySettings - Inherits Global.System.Configuration.ApplicationSettingsBase - - Private Shared defaultInstance As MySettings = CType(Global.System.Configuration.ApplicationSettingsBase.Synchronized(New MySettings()),MySettings) - -#Region "My.Settings Auto-Save Functionality" -#If _MyType = "WindowsForms" Then - Private Shared addedHandler As Boolean - - Private Shared addedHandlerLockObject As New Object - - _ - Private Shared Sub AutoSaveSettings(sender As Global.System.Object, e As Global.System.EventArgs) - If My.Application.SaveMySettingsOnExit Then - My.Settings.Save() - End If - End Sub -#End If -#End Region - - Public Shared ReadOnly Property [Default]() As MySettings - Get - -#If _MyType = "WindowsForms" Then - If Not addedHandler Then - SyncLock addedHandlerLockObject - If Not addedHandler Then - AddHandler My.Application.Shutdown, AddressOf AutoSaveSettings - addedHandler = True - End If - End SyncLock - End If -#End If - Return defaultInstance - End Get - End Property - - _ - Public ReadOnly Property ExecutionFrequency_Minutes() As String - Get - Return CType(Me("ExecutionFrequency_Minutes"),String) - End Get - End Property - - _ - Public ReadOnly Property DebugDetails() As Boolean - Get - Return CType(Me("DebugDetails"),Boolean) - End Get - End Property - - _ - Public ReadOnly Property MFR_UserName() As String - Get - Return CType(Me("MFR_UserName"),String) - End Get - End Property - - _ - Public ReadOnly Property MFR_Password() As String - Get - Return CType(Me("MFR_Password"),String) - End Get - End Property - - _ - Public ReadOnly Property MFR_host() As String - Get - Return CType(Me("MFR_host"),String) - End Get - End Property - End Class -End Namespace - -Namespace My - - _ - Friend Module MySettingsProperty - - _ - Friend ReadOnly Property Settings() As Global.fds.My.MySettings - Get - Return Global.fds.My.MySettings.Default - End Get - End Property - End Module -End Namespace diff --git a/DataService_Legacy/My Project/Settings.settings b/DataService_Legacy/My Project/Settings.settings deleted file mode 100644 index f920057..0000000 --- a/DataService_Legacy/My Project/Settings.settings +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - 15 - - - True - - - system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com - - - 0oT4G3H2 - - - portal.mobilefieldreport.com - - - \ No newline at end of file diff --git a/DataService_Legacy/fds_debug.vb b/DataService_Legacy/fds_debug.vb deleted file mode 100644 index 82ef857..0000000 --- a/DataService_Legacy/fds_debug.vb +++ /dev/null @@ -1,134 +0,0 @@ -Option Explicit On - - - - -Partial Friend Module fds_debug - - - Public Function LogFile(FileName As String) As IO.FileInfo - Return New IO.FileInfo(AppBaseDirectory().FullName & FileName) - End Function - - - - Public Function AppBaseDirectory() As IO.DirectoryInfo - Dim path As String = AppDomain.CurrentDomain.BaseDirectory + "tmp\" - Dim di As New IO.DirectoryInfo(path) - If di.Exists = True Then - Return di - ElseIf My.Computer.FileSystem.DirectoryExists(AppDomain.CurrentDomain.BaseDirectory) = True Then - di.Create() - Return di - Else : Return Nothing - End If - End Function - - - Public Sub DebugLog_async(CodeReference As String, SQLConnectionString As String, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing) - If CodeReference = "" OrElse SQLConnectionString = "" Then Exit Sub - Try - Threading.Tasks.Task.Run(Sub() Call DebugLog_sync(CodeReference:=CodeReference, SQLConnectionString:=SQLConnectionString, exc:=exc, data:=data, context:=context)) - Catch ex As Exception - Call DebugLog_sync(CodeReference:="fds_debug DebugLog_async", SQLConnectionString:=SQLConnectionString, exc:=ex, data:="", context:=Nothing) - End Try - End Sub - - - Public Sub DebugLog_sync(CodeReference As String, SQLConnectionString As String, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing) - If CodeReference = "" OrElse SQLConnectionString = "" Then Exit Sub - Using con As New SqlClient.SqlConnection(SQLConnectionString) - Call DebugLog(CodeReference:=CodeReference, SQLConnection:=con, exc:=exc, data:=data, context:=context) - End Using - End Sub - - - - Public Sub DebugLog(CodeReference As String, SQLConnection As SqlClient.SqlConnection, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing) - If CodeReference = "" OrElse IsNothing(SQLConnection) = True Then Exit Sub - Dim note As String = Now.ToString("yyyy.MM.dd HH:mm:ss") & " - " & CodeReference - Try - Try - If IsNothing(SQLConnection) = False Then - Dim pl As New List(Of SqlClient.SqlParameter) From { - New SqlClient.SqlParameter("@CodeReference", CodeReference), - New SqlClient.SqlParameter("@ExceptionMessage", If(IsNothing(exc), DBNull.Value, exc.Message)), - New SqlClient.SqlParameter("@StackTrace", If(IsNothing(exc), DBNull.Value, exc.StackTrace.ToString)), - New SqlClient.SqlParameter("@data", If(data, DBNull.Value)) - } - Try - Dim w As Integer = 0 - If SQLConnection.State = ConnectionState.Broken Then SQLConnection.Close() - If SQLConnection.State = ConnectionState.Connecting Then - w = 0 - While SQLConnection.State = ConnectionState.Connecting And w < 10 - System.Threading.Thread.Sleep(100) - w += 1 - End While - ElseIf Not SQLConnection.State = ConnectionState.Open Then - SQLConnection.Open() - End If - w = 0 - While Not SQLConnection.State = ConnectionState.Open And w < 10 - System.Threading.Thread.Sleep(100) - w += 1 - End While - Dim cmd As New SqlClient.SqlCommand("EXECUTE [dbo].[fds__admin_logdebug] @CodeReference,@ExceptionMessage,@StackTrace,@Data;", SQLConnection) - cmd.Parameters.AddRange(pl.ToArray) - Call cmd.ExecuteNonQuery() - 'SQLConnection.Close() - cmd.Parameters.Clear() - - Catch sqlex As Exception - End Try - - End If - Catch dbex As Exception - - End Try - - If IsNothing(exc) = False Then - note &= (vbCrLf & "Exception:" & exc.Message & vbCrLf & "Stack:" & exc.StackTrace.ToString).Replace(vbLf, vbLf & " ") - End If - If data <> "" Then - note &= (vbCrLf & "Data:" & data).Replace(vbLf, vbLf & " ") - End If - note &= vbCrLf - - Dim DebugLogfile As IO.FileInfo = LogFile("DebugLog.txt") - If DebugLogfile.Directory.Exists = True Then - IO.File.AppendAllText(DebugLogfile.FullName, note) - End If - Catch logex As Exception - - Finally - - Console.Write(note) - Debug.Print(note) - End Try - End Sub - - Public Sub DebugToFile(note As String, Optional filename As String = "DebugLog.txt") - Try - Dim DebugLogfile As IO.FileInfo = LogFile(filename) - If DebugLogfile.Directory.Exists = True Then - IO.File.AppendAllText(DebugLogfile.FullName, Now.ToString & ": " & note & vbCrLf) - End If - Catch ex As Exception - End Try - End Sub - Public Sub DebugToFile(CodeReference As String, exc As Exception, data As String, Optional filename As String = "DebugLog.txt") - Dim note As String = CodeReference - If IsNothing(exc) = False Then - note &= (vbCrLf & "Exception:" & exc.Message & vbCrLf & "Stack:" & exc.StackTrace.ToString).Replace(vbLf, vbLf & " ") - End If - If data <> "" Then - note &= (vbCrLf & "Data:" & data).Replace(vbLf, vbLf & " ") - End If - - Call DebugToFile(note, filename:=filename) - End Sub - - - -End Module diff --git a/DataService_Legacy/fds_main.vb b/DataService_Legacy/fds_main.vb deleted file mode 100644 index 8d5521c..0000000 --- a/DataService_Legacy/fds_main.vb +++ /dev/null @@ -1,146 +0,0 @@ - -Imports Topshelf -Imports json = Newtonsoft.Json.JsonConvert - - - -Public Class fds_service - Implements Topshelf.ServiceControl - - Dim WithEvents _timer As System.Timers.Timer - - Public Sub New() - Me._timer = New System.Timers.Timer(My.Settings.ExecutionFrequency_Minutes * 60 * 1000) With {.AutoReset = True} - - End Sub - - - Public Function Start(hostControl As HostControl) As Boolean Implements ServiceControl.Start - Me._timer.Start() - Return True - End Function - Public Function StartImmediately(hostControl As HostControl) As Boolean - Me._timer.Start() - System.Threading.Tasks.Task.Run(Sub() - If My.Settings.DebugDetails = True Then Call DebugToFile("fds__data_service - timer started with interval " & _timer.Interval.ToString, filename:="DebugDetail.txt") - Call update_mfr() 'start right away and do not wait until first intervall period is over - End Sub) - 'do not wait and immediately return - Return True - End Function - - Public Function [Stop](hostControl As HostControl) As Boolean Implements ServiceControl.Stop - Me._timer.Stop() - If My.Settings.DebugDetails = True Then System.Threading.Tasks.Task.Run(Sub() Call DebugToFile("fds__data_service - timer stopped", filename:="DebugDetail.txt")) - - Return True - End Function - - - Public Sub timerElapsed() Handles _timer.Elapsed - Call update_mfr() - End Sub - - Friend Shared Sub update_mfr() - If My.Settings.DebugDetails = True Then Call DebugToFile("fds__data_service update_mfr UpdateIfNecessary - timer elapsed", filename:="DebugDetail.txt") - 'call update to data if necessary - Try - Dim t As Threading.Tasks.Task = Threading.Tasks.Task.Run(Async Function() - 'Await UpdateIfNecessary_Single_async(et:=MFR_RESTClient.generic._generic.EntityTypes.Report, DebugDetails:=My.Settings.DebugDetails) - Await UpdateIfNecessary_async(DebugDetails:=My.Settings.DebugDetails) - Await UpdateRequested_async(DebugDetails:=My.Settings.DebugDetails) - Await GetInvoiceFiles_async(DebugDetails:=My.Settings.DebugDetails) - 'Await getDatevZip() - End Function) - t.Wait() - Catch ex As Exception - Call DebugLog("fds__data_service update_mfr UpdateIfNecessary", SQLConnection:=Nothing, exc:=ex) - If My.Settings.DebugDetails = True Then Call DebugToFile("fds__data_service update_mfr UpdateIfNecessary", exc:=ex, data:="", filename:="DebugDetail.txt") - End Try - End Sub - -End Class - - - -Public Module fds_main - - Sub Main() - Dim clArgs() As String = Environment.GetCommandLineArgs() - - If (New String() {"digital-pc", "digital-dpc"}).Contains(Environment.MachineName.ToLower) = False Then - - HostFactory.Run(Sub(x) - x.Service(Of fds_service)(AddressOf ServiceConfiguratorCallback) - x.EnablePauseAndContinue() - x.StartAutomatically() - x.RunAsLocalSystem() - x.SetDescription("MFR Data Sync") - x.SetDisplayName("MFR Data Sync") - x.SetServiceName("MFR Data Sync") - End Sub) - Else - Call fds_service.update_mfr() - 'Call DEv() - End If - End Sub - - Private Sub ServiceConfiguratorCallback(s As ServiceConfigurators.ServiceConfigurator(Of fds_service)) - s.ConstructUsing(Function(name) New fds_service()) - s.WhenStarted(Function(tc, Host) - Return tc.Start(Host) - End Function) - s.WhenStopped(Function(tc, Host) - Return tc.Stop(Host) - End Function) - s.BeforeStoppingService(Sub(HostStopContext) - If My.Settings.DebugDetails = True Then System.Threading.Tasks.Task.Run(Sub() Call DebugToFile("fds__data_service - beforestop", filename:="DebugDetail.txt")) - End Sub) - s.WhenPaused(Function(tc, Host) - Return tc.Stop(Host) - End Function) - s.WhenContinued(Function(tc, Host) - Return tc.StartImmediately(Host) - End Function) - End Sub - - - - Public Sub DEv() - Using MFR As New fds_MFR_Client() - 'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/$metadata")) - 'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/Companies?$top=5&$expand=Contacts,Tags,ServiceObjects,MainContact")) - 'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/ServiceObjects?$expand=WarehouseManager,CustomValueSteps,Company,Product,Tags,ChildServiceObject,Contacts,Items")) - 'Diagnostics.Debug.Print(MFR.ReadAnything(address:="https://portal.mobilefieldreport.com/odata/Contacts/$count")) - 'Diagnostics.Debug.Print(MFR.getEntities()) - Dim fle As Byte() - Try - fle = MFR.GetFile("https://portal.mobilefieldreport.com/mfr/Report/19584712737/Content/") - System.IO.File.WriteAllBytes("C:\Users\sailo\Desktop\Test.pdf", fle) - Catch ex As Exception - - End Try - End Using - - End Sub - - - - - -End Module - - - -Partial Friend Module fds_debug - - - Public Sub DebugLog(CodeReference As String, Optional exc As Exception = Nothing, Optional data As String = "", Optional context As Object = Nothing, Optional execute_async As Boolean = True) - If execute_async = True Then - Call DebugLog_async(CodeReference:=CodeReference, SQLConnectionString:=FDSConnectionString(), exc:=exc, data:=data, context:=context) - Else - Call DebugLog_sync(CodeReference:=CodeReference, SQLConnectionString:=FDSConnectionString(), exc:=exc, data:=data, context:=context) - End If - End Sub - -End Module diff --git a/DataService_Legacy/fds_mfr.vb b/DataService_Legacy/fds_mfr.vb deleted file mode 100644 index 78d2eda..0000000 --- a/DataService_Legacy/fds_mfr.vb +++ /dev/null @@ -1,1046 +0,0 @@ - - -Imports System.Configuration -Imports MFR_RESTClient -Imports MFR_RESTClient.generic -Imports Newtonsoft.Json.Linq -Imports json = Newtonsoft.Json.JsonConvert -Imports vbs = Microsoft.VisualBasic.Strings -Imports OCMS -Imports OCMS.SQLHandling -Imports System.Text - -Namespace Global.fds - - Public Class fds_SQLOptions - Inherits sqloptions - - Dim baseinfo As New Dictionary(Of String, Object) - - Public Sub New(Optional dic As Dictionary(Of String, Object) = Nothing) - MyBase.New() - Me.baseinfo = dic - Me.OnError = Sub(procedure As String, ex As Exception, data As Object) - Call debug_log(procedure:=procedure, ex:=ex, data:=New With {.ctrl_nfo = dic, .sql = data}) - End Sub - End Sub - End Class - - Public Module fds_mfr - - Friend Function AppDirectory() As String - Return AppDomain.CurrentDomain.BaseDirectory - End Function - - Public Enum UpdateNeed - [Reset] = 5 - [Full] = 2 - [Short] = 1 - [None] = 0 - End Enum - - -#Disable Warning BC42358 - Public Async Function UpdateIfNecessary_async(Optional DebugDetails As Boolean = False) As Threading.Tasks.Task - - Using mfr As New fds_MFR_Client - Try - - If DebugDetails = True Then Call DebugToFile("UpdateIfNecessary_async - unn - start awaited", filename:="DebugDetail.txt") - Await mfr.Update__Entitytables(DebugDetails:=DebugDetails) - If DebugDetails = True Then Call DebugToFile("UpdateIfNecessary_async - unn - completed", filename:="DebugDetail.txt") - - Catch ex As Exception - Call DebugLog("UpdateIfNecessary_async - main unn", exc:=ex) - If DebugDetails = True Then Call DebugToFile("UpdateIfNecessary_async - unn", exc:=ex, data:="", filename:="DebugDetail.txt") - End Try - End Using - - End Function - Public Async Function UpdateRequested_async(Optional DebugDetails As Boolean = False) As Threading.Tasks.Task - - Using mfr As New fds_MFR_Client - Try - - If DebugDetails = True Then Call DebugToFile("UpdateRequested_async - unn - start awaited", filename:="DebugDetail.txt") - Await mfr.Update__EntityRequests(DebugDetails:=DebugDetails) - If DebugDetails = True Then Call DebugToFile("UpdateRequested_async - unn - completed", filename:="DebugDetail.txt") - - Catch ex As Exception - Call DebugLog("UpdateRequested_async - main unn", exc:=ex) - If DebugDetails = True Then Call DebugToFile("UpdateRequested_async - unn", exc:=ex, data:="", filename:="DebugDetail.txt") - End Try - End Using - - End Function - - Public Async Function GetInvoiceFiles_async(Optional DebugDetails As Boolean = False) As Threading.Tasks.Task - - Using mfr As New fds_MFR_Client - Try - - If DebugDetails = True Then Call DebugToFile("GetInvoiceFiles_async - unn - start awaited", filename:="DebugDetail.txt") - Dim dtbl As SQLDataTable = Await getSQLDatatable_async("EXECUTE [dbo].[fds__fn_getMFRInvoicesWithoutfiles];", FDSConnectionString, SqlParameterList:=Nothing, options:=New fds_SQLOptions()) - If dtbl.Count > 0 Then - For Each ivrw As DataRow In dtbl.DataTable.Rows - Dim id As String = ivrw.nz("id"), DocumentName As String = ivrw.nz("DocumentName"), fileurl As String = ivrw.nz("URI") - If id <> "" AndAlso DocumentName <> "" AndAlso fileurl <> "" AndAlso DocumentName.EndsWith("pdf") Then - Dim fl As Byte() = mfr.GetFile(fileurl) - If IsNothing(fl) = False AndAlso fl.Length > 0 Then - Try - Await setSQLValue_async("EXECUTE [dbo].[fds__setMFRInvoiceFile] @Id, @filename, @file;", FDSConnectionString, SqlParameterList:=New ParamList(SQL_VarChar("@Id", id), SQL_VarChar("@filename", DocumentName), New SqlClient.SqlParameter("@file", fl) With {.SqlDbType = SqlDbType.VarBinary}), options:=New fds_SQLOptions()) - Catch fsex As Exception - Call DebugLog("GetInvoiceFiles_async - mfr storefile", exc:=fsex) - End Try - - End If - End If - Next - End If - If DebugDetails = True Then Call DebugToFile("GetInvoiceFiles_async - unn - completed", filename:="DebugDetail.txt") - - Catch ex As Exception - Call DebugLog("GetInvoiceFiles_async - main unn", exc:=ex) - If DebugDetails = True Then Call DebugToFile("GetInvoiceFiles_async - unn", exc:=ex, data:="", filename:="DebugDetail.txt") - End Try - End Using - - End Function - - Public Async Function UpdateIfNecessary_Single_async(et As EntityTypes, Optional DebugDetails As Boolean = False) As Threading.Tasks.Task(Of Boolean) - Dim ret As Boolean = False - Using mfr As New fds_MFR_Client - Try - - If DebugDetails = True Then Call DebugToFile("UpdateIfNecessary_async - unn - start awaited", filename:="DebugDetail.txt") - Await mfr.Update__Entitytables(DebugDetails:=DebugDetails, tgtEntityType:=et) - If DebugDetails = True Then Call DebugToFile("UpdateIfNecessary_async - unn - completed", filename:="DebugDetail.txt") - ret = True - Catch ex As Exception - Call DebugLog("UpdateIfNecessary_async - main unn", exc:=ex) - If DebugDetails = True Then Call DebugToFile("UpdateIfNecessary_async - unn", exc:=ex, data:="", filename:="DebugDetail.txt") - ret = False - End Try - End Using - Return ret - End Function - - Public Async Function getSchema() As Task(Of String) - Dim ret As String = "" - Using mfr As New fds_MFR_Client - ret = Await mfr.ReadAnything(address:=mfr.clientConfig.BaseUrl & "$metadata") - End Using - Return ret - End Function - - Public Async Function getAnything(query As String, Optional throwerror_if_nOK As Boolean = True) As Task(Of String) - Dim ret As String = "" - Using mfr As New fds_MFR_Client - ret = Await mfr.ReadAnything(address:=mfr.clientConfig.BaseUrl & query, throwerror_if_nOK:=throwerror_if_nOK) - End Using - Return ret - End Function - - - Public Function getReportDoc(ByRef file As Byte(), reportid As String, Optional DebugDetails As Boolean = False) As IO.FileInfo - Dim pl As New List(Of SqlClient.SqlParameter) From {SQL_VarChar("@reportid", reportid)} - Dim sqldt As SQLDataTable = System.Threading.Tasks.Task.Run(Async Function() Await getSQLDatatable_async("EXECUTE [dbo].[fds__getReportDocument] @reportid;", FDSConnectionString, SqlParameterList:=pl, options:=New fds_SQLOptions())).Result() - If sqldt.Count > 0 Then - Dim irow As DataRow = sqldt.FirstRow() - Dim fl As Byte() = Nothing, fln As String = irow.nz("DocumentName") - Try - - If fln <> "" AndAlso irow.nz("fds") = False Then - Using mfr As New fds_MFR_Client - fl = mfr.GetFile(irow.nz("URI")) - End Using - If IsNothing(fl) = False AndAlso fl.Length > 0 Then - Try - Dim r = System.Threading.Tasks.Task.Run(Async Function() Await setSQLValue_async("EXECUTE [dbo].[fds__setMFRInvoiceFile] @Id, @filename, @file;", FDSConnectionString, SqlParameterList:=New ParamList(SQL_VarChar("@Id", reportid), SQL_VarChar("@filename", fln), New SqlClient.SqlParameter("@file", fl) With {.SqlDbType = SqlDbType.VarBinary}), options:=New fds_SQLOptions())).Result() - Catch fsex As Exception - Call DebugLog("getReportDoc - mfr storefile", exc:=fsex) - End Try - - End If - Else - fl = irow.no("file", Nothing) - End If - Catch ex As Exception - Call DebugLog("getReportDoc - mfr", exc:=ex) - If DebugDetails = True Then Call DebugToFile("getReportDoc - mfr", exc:=ex, data:="", filename:="DebugDetail.txt") - End Try - If IsNothing(fl) = False Then - file = fl - Dim ReportFile As New IO.FileInfo(IO.Path.GetTempPath & fln.Replace("

", "").Replace("

", "").Replace("=", "_").Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("<", "_").Replace("/", "_")) - Return ReportFile - Else - Return Nothing - End If - Else - Return Nothing - End If - End Function - Public Function getFDSDoc(ByRef file As Byte(), reportid As String, type As String) As IO.FileInfo - Dim pl As New List(Of SqlClient.SqlParameter) From {SQL_VarChar("@type", type), SQL_VarChar("@reportid", reportid)} - Dim sqldt As SQLDataTable = System.Threading.Tasks.Task.Run(Async Function() Await getSQLDatatable_async("EXECUTE [dbo].[fds__getFDSDocument] @type, @reportid;", FDSConnectionString, SqlParameterList:=pl, options:=New fds_SQLOptions())).Result() - If sqldt.Count > 0 Then - Dim irow As DataRow = sqldt.FirstRow() - Dim fl As Byte() = irow.no("file", Nothing), fln As String = irow.nz("DocumentName") - If IsNothing(fl) = False Then - file = fl - Dim ReportFile As New IO.FileInfo(IO.Path.GetTempPath & fln.Replace("=", "_").Replace(">", "_").Replace("|", "_").Replace("!", "_").Replace("?", "_")) - Return ReportFile - Else - Return Nothing - End If - Else - Return Nothing - End If - End Function - - Public Function DATEV(header As DATEV_HEADER, tbl As DataTable) As String - - Return header.toHeaderstring() & ControlChars.CrLf & tbl.ToCsv(includeHeaders:=True, quoteStrings:=False, fieldDelimiter:=";", cultureinfo:=New Globalization.CultureInfo("de-DE"), quoteHeader:=False) - End Function - Public Class DATEV_HEADER - - 'c.f. https://developer.datev.de/portal/de/dtvf/formate/header - - Public Kennzeichen As DATEV_Kennzeichen = DATEV_Kennzeichen.EXTF - Public Versionsnummer As Integer = 700 - Public Formatkategorie As DATEV_Formatkategorie = DATEV_Formatkategorie.Debitoren__Kreditoren - Public Formatversion As DATEV_Formatversion = DATEV_Formatversion.Debitoren__Kreditoren - Public Beraternummer As Integer - Public Mandantennummer As Integer - Public WJBeginn As Date - Public Sachkontenlänge As Integer - Public Datum_von As Date - Public Datum_bis As Date - Public Bezeichnung As String - Public Buchungstyp As DATEV_Buchungstyp = DATEV_Buchungstyp.Finanzbuchführung - Public Rechnungsslegungszweck As DATEV_Rechnungslegungszweck = DATEV_Rechnungslegungszweck.unabhängig - Public Festschreibung As Boolean = 1 - Public WKZ As String = "EUR" - - - Public created As Date = Now() - Public ReadOnly Property Formatname As String - Get - Return [Enum].GetName(GetType(DATEV_Formatkategorie), Me.Formatkategorie).Replace("__", "/").Replace("_", " ") - End Get - End Property - - Dim EmptyDblQuote As String = Microsoft.VisualBasic.ChrW(34) & Microsoft.VisualBasic.ChrW(34) - - Public ReadOnly Property toHeaderstring As String - Get - Dim sb As New List(Of String) - sb.Add(Microsoft.VisualBasic.ChrW(34) & [Enum].GetName(GetType(DATEV_Kennzeichen), Me.Kennzeichen) & Microsoft.VisualBasic.ChrW(34)) - sb.Add(Me.Versionsnummer.ToString) - sb.Add(CInt(Me.Formatkategorie).ToString) - sb.Add(Microsoft.VisualBasic.ChrW(34) & Me.Formatname & Microsoft.VisualBasic.ChrW(34)) - sb.Add(CInt(Me.Formatversion).ToString) - sb.Add(created.ToString("yyyyMMddHHmmssfff")) - sb.Add("") '7 Leerfeld - sb.Add(EmptyDblQuote) '8 Leerfeld - sb.Add(EmptyDblQuote) '9 Leerfeld - sb.Add(EmptyDblQuote) '10 Leerfeld - sb.Add(Me.Beraternummer.ToString) '11 Beraternummer - sb.Add(Me.Mandantennummer.ToString) '12 Mandantennummer - sb.Add(Me.WJBeginn.ToString("yyyyMMdd")) - sb.Add(Me.Sachkontenlänge.ToString) - sb.Add(Me.Datum_von.ToString("yyyyMMdd")) - sb.Add(Me.Datum_bis.ToString("yyyyMMdd")) - sb.Add(Microsoft.VisualBasic.ChrW(34) & Me.Bezeichnung & Microsoft.VisualBasic.ChrW(34)) - sb.Add(EmptyDblQuote) '18 Diktatkürzel - sb.Add(CInt(Me.Buchungstyp).ToString) - sb.Add(CInt(Me.Rechnungsslegungszweck).ToString) - sb.Add(If(Me.Festschreibung = False, 0, 1)) - sb.Add(Microsoft.VisualBasic.ChrW(34) & Me.WKZ & Microsoft.VisualBasic.ChrW(34)) - sb.Add("") '23 Leerfeld - sb.Add("") '24 Leerfeld - sb.Add("") '25 Leerfeld - sb.Add("") '26 Leerfeld - sb.Add("") '27 Sachkontenrahmen - sb.Add("") '28 ID der Branchenlösung - sb.Add("") '29 Leerfeld - sb.Add("") '30 Leerfeld - sb.Add("") '31 Verarbeitungskennzeichen - Return vbs.Join(sb.ToArray, ";") - End Get - End Property - - - End Class - - - - Public Enum DATEV_Rechnungslegungszweck - unabhängig = 0 - Steuerrecht = 30 - Kalkulatorik = 40 - Handelsrecht = 50 - IFRS = 64 - End Enum - - Public Enum DATEV_Buchungstyp - Finanzbuchführung = 1 - Jahresabschluss = 2 - End Enum - - Public Enum DATEV_Kennzeichen - EXTF - DTVF - End Enum - - Public Enum DATEV_Formatkategorie - [Debitoren__Kreditoren] = 16 - [Sachkontenbeschriftungen] = 20 - [Buchungsstapel] = 21 - [Zahlungsbedingungen] = 46 - [Diverse_Adressen] = 48 - [Wiederkehrende_Buchungen] = 65 - End Enum - Public Enum DATEV_Formatversion - [Debitoren__Kreditoren] = 5 - [Sachkontenbeschriftungen] = 3 - [Buchungsstapel_9] = 9 - [Buchungsstapel] = 12 - [Zahlungsbedingungen] = 2 - [Wiederkehrende_Buchungen] = 4 - [Diverse_Adressen] = 2 - End Enum - - Public Class DATEV_Document - Public guid As String - Public type As Integer = 2 - Public processID As Integer = 2 - Public Filename As String - Public KeyWords As String - Public Repository As String - Public Register As String - Public Sub New(guid As String, filename As String, keywords As String, Repository As String, Register As String) - Me.guid = guid - Me.Filename = filename - Me.KeyWords = keywords - Me.Repository = Repository - Me.Register = Register - End Sub - End Class - - - Public Function getDatevZip(ByRef stream As IO.Stream, ByVal tgtdate As Date, mode As String, ByVal AuthUser As String, ByVal includeFiles As Boolean, Optional DebugDetails As Boolean = False) As IO.FileInfo - - Using mfr As New fds_MFR_Client - Try - Dim DSet As SQLDataSet = System.Threading.Tasks.Task.Run(Async Function() Await getSQLDataSet_async("EXECUTE [dbo].[fds__getDatevExports] @tgtdate, @mode, @files, @authuser;", FDSConnectionString, SqlParameterList:=New List(Of SqlClient.SqlParameter) From {SQL_date("@tgtdate", tgtdate), SQL_VarChar("@mode", mode), SQL_bit("@files", includeFiles), SQL_VarChar("@authuser", AuthUser)}, tablenames:=New String() {"admin", "inv", "buchungen", "debitoren"}, options:=New fds_SQLOptions())).Result() - - Dim bedi_files As New List(Of DATEV_Document) - - If DSet.Count >= 4 Then - Dim admin As Dictionary(Of String, Object) = DSet.Tables("admin").Rows(0).toObjectDictionary() - Dim startdate As Date = admin("startdate"), enddate As Date = admin("enddate") - Dim Datev_Level3_Register As String = tgtdate.ToString("yyyy\/MM") & If(mode.ToLower = "w", "_w" & tgtdate.ToString("dd"), "") - Dim fls As New Dictionary(Of String, Byte()) - Dim header As New DATEV_HEADER With { - .Formatkategorie = DATEV_Formatkategorie.Buchungsstapel, - .Formatversion = DATEV_Formatversion.Buchungsstapel_9, - .Beraternummer = admin("beraternummer"), - .Mandantennummer = admin("mandantennummer"), - .WJBeginn = admin("WJ-Beginn"), - .Sachkontenlänge = admin("Sachkontenlänge"), - .Datum_von = startdate, - .Datum_bis = enddate, - .Bezeichnung = "fds_" & mode & tgtdate.ToString("yyMMdd") - } - fls.Add("EXTF_PCW_Buchungen_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", DATEV(header, DSet.Tables("buchungen")).ToByteArray(encoding:=Encoding.GetEncoding("ISO-8859-1"))) - - ''only for debug - 'Using fsw As New IO.StreamWriter("C:\Users\sailo\Desktop\Fuchs\DatevUpload_AR 202107_w___DATEVNEU\EXTF_PCW_Buchungen_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", False, Encoding.GetEncoding("ISO-8859-1")) - ' fsw.Write(value:=DATEV(header, DSet.Tables("buchungen"))) - ' fsw.Close() - 'End Using - - - With header - .Formatkategorie = DATEV_Formatkategorie.Debitoren__Kreditoren - .Formatversion = DATEV_Formatversion.Debitoren__Kreditoren - End With - fls.Add("EXTF_PCW_Debitoren_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", DATEV(header, DSet.Tables("debitoren")).ToByteArray(encoding:=Encoding.GetEncoding("ISO-8859-1"))) - - - ''only for debug - 'Using fsw As New IO.StreamWriter("C:\Users\sailo\Desktop\Fuchs\DatevUpload_AR 202107_w___DATEVNEU\EXTF_PCW_Debitoren_0_" & startdate.ToString("yyMMdd HHmmss") & ".csv", False, Encoding.GetEncoding("ISO-8859-1")) - ' fsw.Write(value:=DATEV(header, DSet.Tables("debitoren"))) - ' fsw.Close() - 'End Using - - - 'add csv files to bedi list - For Each fl As String In fls.Keys - bedi_files.Add(New DATEV_Document(Guid.NewGuid().ToString.ToLower, filename:=fl, keywords:="", Repository:="Processweb_Daten", Register:=Datev_Level3_Register)) - Next - - If includeFiles = True Then - For Each irow As DataRow In DSet.Tables("inv").Rows - Dim fl As Byte() = Nothing, fln As String = irow.nz("DocumentName") - Try - If fln <> "" AndAlso irow.no("fds", True) = False Then - fl = mfr.GetFile(irow.nz("URI")) - Else - fl = irow.no("file", Nothing) - End If - Catch ex As Exception - - End Try - If IsNothing(fl) = False AndAlso fln <> "" Then - fls.Add(fln, fl) - bedi_files.Add(New DATEV_Document(irow("file_guid"), filename:=fln, keywords:="RgNr: " & irow.nz("InvoiceId") & " RgDatum: " & irow.ndt_string("DateOfCreation", "dd.MM.yyyy"), Repository:="ProcessWeb_Belege", Register:=Datev_Level3_Register)) - End If - 'If fls.Count > 7 Then Exit For - Next - - fls.Add("document.xml", createDATEV_document_xml(bedi_files).ToByteArray()) - End If - - If fls.Count > 0 Then - Dim ArchiveFile As New IO.FileInfo(IO.Path.GetTempPath & "DatevUpload_AR " & startdate.ToString("yyyyMM") & "_" & admin("mode").ToString & ".zip") - Try - Using Archive As New fds.Archive(ArchiveFile, Type:=SevenZip.OutArchiveFormat.Zip) - If Archive.CompressToStream(fls, targetstream:=stream) = True Then - Diagnostics.Debug.Print("ok") - 'Archive.WriteArchiveStreamToDisk() - stream.Position = 0 - Return ArchiveFile - Else - Return Nothing - End If - End Using - Catch aex As Exception - End Try - - End If - - End If - - Catch ex As Exception - Call DebugLog("getDatevZip - mfr", exc:=ex) - If DebugDetails = True Then Call DebugToFile("getDatevZip - mfr", exc:=ex, data:="", filename:="DebugDetail.txt") - End Try - End Using - Return Nothing - End Function -#Enable Warning BC42358 - - Public Function createDATEV_document_xml(files As List(Of DATEV_Document)) As String - Dim xmldoc As New Xml.XmlDocument - - 'Write down the XML declaration - Dim XmlDeclaration As Xml.XmlDeclaration = xmldoc.CreateXmlDeclaration("1.0", "UTF-8", Nothing) - xmldoc.InsertBefore(XmlDeclaration, xmldoc.DocumentElement) - - 'create root - Dim RootNode As Xml.XmlElement = xmldoc.CreateElement("archive", "http://xml.datev.de/bedi/tps/document/v04.0") - xmldoc.AppendChild(RootNode) - - With RootNode - Dim schemaLocation As Xml.XmlAttribute = xmldoc.CreateAttribute("xsi", "schemaLocation", "http://www.w3.org/2001/XMLSchema-instance") - schemaLocation.Value = "http://xml.datev.de/bedi/tps/document/v04.0 document_v040.xsd" - .Attributes.Append(schemaLocation) - .SetAttribute("generatingSystem", "ProcessWeb") - .SetAttribute("version", "4.0") - End With - - Dim header As Xml.XmlElement = xmldoc.CreateElement("header") - xmldoc.DocumentElement.PrependChild(header) - Dim headerelement As Xml.XmlElement = xmldoc.CreateElement("date") - headerelement.InnerText = Now().ToString("yyyy-MM-ddTHH:mm:ss") - header.AppendChild(headerelement) - headerelement = xmldoc.CreateElement("description") - headerelement.InnerText = "DATEV Schnittstelle ProcessWeb" - header.AppendChild(headerelement) - - Dim content As Xml.XmlElement = xmldoc.CreateElement("content") - xmldoc.DocumentElement.AppendChild(content) - For Each fl As DATEV_Document In files - Dim docelement As Xml.XmlElement = xmldoc.CreateElement("document") - docelement.SetAttribute("guid", fl.guid) - docelement.SetAttribute("type", fl.type) - docelement.SetAttribute("processID", fl.processID) - - If If(fl.KeyWords, "") <> "" Then - Dim keyword As Xml.XmlElement = xmldoc.CreateElement("keywords") - keyword.InnerText = fl.KeyWords - docelement.AppendChild(keyword) - End If - - Dim extension As Xml.XmlElement = xmldoc.CreateElement("extension") - Dim ext_attr As Xml.XmlAttribute = xmldoc.CreateAttribute("xsi", "type", "http://www.w3.org/2001/XMLSchema-instance") - ext_attr.Value = "File" - extension.Attributes.Append(ext_attr) - extension.SetAttribute("name", fl.Filename) - docelement.AppendChild(extension) - - Dim repository As Xml.XmlElement = xmldoc.CreateElement("repository") - Dim level As Xml.XmlElement - level = xmldoc.CreateElement("level") - level.SetAttribute("id", "1") 'Kategorie - level.SetAttribute("name", "ProcessWeb") - repository.AppendChild(level) - level = xmldoc.CreateElement("level") - level.SetAttribute("id", "2") 'Ordner - level.SetAttribute("name", fl.Repository) - repository.AppendChild(level) - level = xmldoc.CreateElement("level") - level.SetAttribute("id", "3") 'Register e.g. 2020/12 - level.SetAttribute("name", fl.Register) - repository.AppendChild(level) - docelement.AppendChild(repository) - - content.AppendChild(docelement) - Next - - Dim xmlasstring As String = "" - Using sw As New IO.StringWriter - Using tw As Xml.XmlWriter = Xml.XmlWriter.Create(sw) - xmldoc.WriteTo(tw) - tw.Flush() - xmlasstring = sw.GetStringBuilder().ToString() - End Using - End Using - Return xmlasstring - End Function - - - Public Function UpdateNeedValue(need As String) As UpdateNeed - Return [Enum].Parse(GetType(UpdateNeed), need) - End Function - - - - End Module - - - - - - Public Class fds_MFR_Client - Implements IDisposable - - - Dim MFRClient As Global.MFR_RESTClient.MFRClient - - Private Shared Function DefaultCredentials() - Return New MFRClientCredentials(My.Settings.MFR_UserName, My.Settings.MFR_Password) - End Function - - Public Sub New(Credentials As MFRClientCredentials) - Dim config As New MFRClientConfig(My.Settings.MFR_host) - Me.MFRClient = New MFRClient(config, credentials:=Credentials) - End Sub - Public Sub New() - Me.New(fds_MFR_Client.DefaultCredentials()) - End Sub - - Public Property IsReadonly() As Boolean - Get - Return Me.MFRClient.IsReadonly - End Get - Set(value As Boolean) - Me.MFRClient.IsReadonly = value - End Set - End Property - - Public ReadOnly Property clientConfig As MFRClientConfig - Get - Return Me.MFRClient.clientConfig - End Get - End Property - - Public Async Function ReadAnything(address As String, Optional throwerror_if_nOK As Boolean = True) As Task(Of String) - Return Await Me.MFRClient.ReadAnything(address:=address, throwerror_if_nOK:=throwerror_if_nOK) - End Function - Public Function GetFile(address As String, Optional throwerror_if_nOK As Boolean = True) As Byte() - Return Me.MFRClient.GetFile(address:=address, throwerror_if_nOK:=throwerror_if_nOK) - End Function - Public Async Function ReadOData(address As String, Optional throwerror_if_nOK As Boolean = True) As Task(Of ODataEnvelope) - Return Await Me.MFRClient.ReadOData(address:=address, throwerror_if_nOK:=throwerror_if_nOK) - End Function - Public Async Function getEntities(Optional throwerror_if_nOK As Boolean = True) As Task(Of String) - Return Await Me.MFRClient.getEntities(throwerror_if_nOK:=throwerror_if_nOK) - End Function - - Public Class DatabaseSchema - Private _et As EntityTypes - - Public ReadOnly Property isValid As Boolean = False - Public ReadOnly Property hasEntity As Boolean = False - - Public ReadOnly Property ThisEntityName As String - Get - Return EntityName(Me._et) - End Get - End Property - - Public ReadOnly Property EntityConfig As Dictionary(Of String, String) - Public ReadOnly Property Entitytablename As String - Public ReadOnly Property Complextypes As New Dictionary(Of String, Dictionary(Of String, String)) - Public ReadOnly Property NavProperties As New Dictionary(Of String, Dictionary(Of String, String)) - - Private _tableset As DataSet - - - - Public Sub New(et As EntityTypes) - Me._et = et - Dim DSet As SQLDataSet = System.Threading.Tasks.Task.Run(Async Function() - Return Await getSQLDataSet_async("EXECUTE [dbo].[mfr__getSchema] 'table', @tgttype;", FDSConnectionString, SqlParameterList:=New List(Of SqlClient.SqlParameter) From {New SqlClient.SqlParameter("@tgttype", ThisEntityName)}, tablenames:=New String() {"entity", "complex_types", "navigation_properties", "tables"}, options:=New fds_SQLOptions()) - End Function).Result() - - Me._isValid = DSet.Count > 0 - Me._hasEntity = DSet.Tables("entity").Rows.Count = 1 - - Me._EntityConfig = If(DSet.Tables("entity").Rows.Count > 0, DSet.Tables("entity").Rows(0).toStringDictionary(), New Dictionary(Of String, String)) - Me._Entitytablename = If(Me.EntityConfig.ContainsKey("tablename"), Me.EntityConfig("tablename"), "") - - For Each ctrw As DataRow In DSet.Tables("complex_types").Rows() - Me._Complextypes.Add(ctrw.nz("name").ToLower, ctrw.toStringDictionary()) - Next - For Each nprw As DataRow In DSet.Tables("navigation_properties").Rows() - Me._NavProperties.Add(nprw.nz("name").ToLower, nprw.toStringDictionary()) - Next - Dim sql As New List(Of String) From {newDatatable_SQL(Me.Entitytablename)} 'The target entitytype's table MUST be the first - Dim tablenames As New List(Of String) From {Me.Entitytablename} - - For Each ttrw As DataRow In DSet.Tables("tables").Rows - sql.Add(newDatatable_SQL(ttrw.nz("tablename"))) - tablenames.Add(ttrw.nz("tablename")) - Next - Me._tableset = System.Threading.Tasks.Task.Run(Async Function() - Return Await getSQLDataSet_async(vbs.Join(sql.ToArray(), vbNewLine), FDSConnectionString, tablenames:=tablenames.ToArray, options:=New fds_SQLOptions()) - End Function).Result().DataSet - End Sub - - Public Function tgtDataset(SetID As String) As DataSet - Dim dset As New DataSet - For Each tbl As DataTable In Me._tableset.Tables - Dim t As DataTable = tbl.Clone - If t.Columns.Contains("setid") Then t.Columns("setid").DefaultValue = SetID - dset.Tables.Add(t) - Next - Return dset - End Function - - End Class - - -#Disable Warning BC42356 ' This async method lacks 'Await' operators and so will run synchronously - Public Async Function Update__entitytable(et As EntityTypes, UpdateNeed As fds.fds_mfr.UpdateNeed, EntityID As Long(), Optional DebugDetails As Boolean = False, Optional SchemaDic As Dictionary(Of String, DatabaseSchema) = Nothing, Optional AdditionalCommandAfter As String = "") As Threading.Tasks.Task(Of Boolean) - If et = EntityTypes.none Then Return False - EntityID = If(EntityID, New Long() {}) ''make sure is not nothing - If UpdateNeed = UpdateNeed.None AndAlso If(EntityID, New Long() {}).Length = 0 Then Return True - - Dim StartTime As DateTime = Now() - Diagnostics.Debug.Print(StartTime.ToLongTimeString() & " - " & "Start before Schema " & et.ToString) - Dim SetID As String = RandomString(5) 'generate a unique ID to identify the submitted data and prevent mixup. A table can be accessed by several batch processes in parallel. - Dim ThisEntityName As String = EntityName(et) - - - Dim DtF As New Action(Of String, String, String)(Sub(note As String, info As String, data As String) - If DebugDetails = True Then Call DebugToFile("Update__entitytable - " & note & " For " & ThisEntityName & "(" & SetID & ")" & If(If(info, "") <> "", ": " & info, ""), filename:="DebugDetail.txt") - End Sub) - Dim Dlg As New Action(Of String, String, String)(Sub(note As String, info As String, data As String) - Dim str As String = "Update__entitytable - " & note & " for " & ThisEntityName & "(" & SetID & ")" & If(If(info, "") <> "", ": " & info, "") - Call DebugLog(str, data:=If(data, "")) - If DebugDetails = True Then Call DebugToFile(str, filename:="DebugDetail.txt") - Diagnostics.Debug.Print(str) - End Sub) - - - - 'check if update processes not already in progress and set lock - Dim params As New List(Of SqlClient.SqlParameter) From { - New SqlClient.SqlParameter("@tbl", ThisEntityName), - New SqlClient.SqlParameter("@state", True), - New SqlClient.SqlParameter("@override", False), - New SqlClient.SqlParameter("@setid", SetID) - } - Dim lockstate As Object = True 'getSQLValue("EXECUTE [dbo].[ctm__admin_getTableLock] @tbl, @state, @override, @setid;", con, SqlParameterList:=params, options:=New fds_SQLOptions()) - If DebugDetails = True AndAlso lockstate.GetType() = GetType(System.Boolean) Then Call DebugToFile("Update__entitytable - lock received for " & ThisEntityName & "(" & SetID & ") - " & lockstate.ToString, filename:="DebugDetail.txt") - 'true means lock was successful - If lockstate.GetType() = GetType(System.Boolean) AndAlso DirectCast(lockstate, Boolean) = True Then - Try - 'get Schema - Dim Schema As DatabaseSchema = If(IsNothing(SchemaDic) = False AndAlso SchemaDic.ContainsKey(ThisEntityName), SchemaDic(ThisEntityName), New DatabaseSchema(et)) - If Schema.isValid = False Then - Dlg("Schema not found", "", Nothing) - ElseIf Schema.hasEntity = True Then - Dim tgtDataset As DataSet = Schema.tgtDataset(SetID:=SetID) - - Dim lastDate As SQLObject, Filter As New List(Of String), EntityIDFilter As String = "", isProcessed As Boolean = False - If If(EntityID, New Long() {}).Length = 1 Then - EntityIDFilter = "(" & EntityID(0).ToString() & "L)" - Filter.Add("") - UpdateNeed = UpdateNeed.Reset - ElseIf If(EntityID, New Long() {}).Length > 1 Then - EntityIDFilter = "" - For idx As Integer = 0 To EntityID.Length - 1 Step 50 - Filter.Add("$filter=" & EntityID.Skip(idx).Take(50).Convert(Of String)(Function(l As Long) "Id eq " & l.ToString() & "L").Join(" or ")) - Next - UpdateNeed = UpdateNeed.Reset - ElseIf UpdateNeed > UpdateNeed.Short Then - EntityIDFilter = "" - Filter.Add("") - ElseIf Schema.EntityConfig.ContainsKey("DateColumn") AndAlso If(Schema.EntityConfig("DateColumn"), "") <> "" Then - lastDate = Await getSQLValue_async(Schema.EntityConfig.nz("DateSQL").ne("SELECT MAX([" & Schema.EntityConfig("DateColumn") & "]) FROM [dbo].[" & Schema.Entitytablename & "];"), FDSConnectionString, options:=New fds_SQLOptions()) - Filter.Add(If(UpdateNeed = fds.fds_mfr.UpdateNeed.Short AndAlso IsNothing(lastDate.Result) = False AndAlso lastDate.Result.GetType() = GetType(System.DateTime), "$filter=" & Schema.EntityConfig("DateColumn") & " gt DateTime'" & DirectCast(lastDate.Result, System.DateTime).ToString(format:="yyyy\-MM\-dd") & "T00:00:00'", "")) - End If - For Each iFilter As String In Filter - If (Schema.EntityConfig.ContainsKey("url") AndAlso Schema.EntityConfig("url") <> "") AndAlso (UpdateNeed = UpdateNeed.Short AndAlso iFilter <> "") OrElse (UpdateNeed > UpdateNeed.Short) Then - isProcessed = True - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "Start MFR " & Schema.Entitytablename) - Dim MFR_Response As ODataEnvelope = Await ReadOData(Me.MFRClient.clientConfig.BaseUrl & Schema.EntityConfig("url").Replace("?", EntityIDFilter & "?") & If(iFilter <> "" AndAlso Schema.EntityConfig("url").Contains("?") = False, "?", If(iFilter <> "", "&", "")) & iFilter) - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "End MFR " & Schema.Entitytablename) - Do Until IsNothing(MFR_Response) = True - If MFR_Response.value.GetType() = GetType(JObject) Then - MFR_Response.ConvertToArray() - End If - Try - - If MFR_Response.value.GetType() = GetType(JArray) AndAlso IsNothing(DirectCast(MFR_Response.value, Newtonsoft.Json.Linq.JContainer).First) = False AndAlso DirectCast(DirectCast(DirectCast(MFR_Response.value, Newtonsoft.Json.Linq.JContainer).First, Newtonsoft.Json.Linq.JContainer).First, Newtonsoft.Json.Linq.JProperty).Name = "odata.error" AndAlso DirectCast(MFR_Response.value, Newtonsoft.Json.Linq.JContainer).First.ToString().IndexOf("Sequence contains no elements", 0, StringComparison.InvariantCultureIgnoreCase) > -1 Then - MFR_Response.value = New JArray() - End If - Catch jbex As Exception - End Try - If MFR_Response.value.GetType() = GetType(JArray) Then - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & DirectCast(MFR_Response.value, JArray).Count & ":" & tgtDataset.Tables(Schema.Entitytablename).Rows.Count & "- this " & Strings.Right(MFR_Response.url, 10)) - For Each vi As JObject In MFR_Response.value - Dim vdic As Dictionary(Of String, Object) = vi.ToObject(Of Dictionary(Of String, Object)) - Dim etrow As DataRow = tgtDataset.Tables(Schema.Entitytablename).NewRow - etrow.Item("setid") = SetID - etrow.Item("Id") = vdic("Id") - For Each vkey As String In vdic.Keys - If IsNothing(vdic(vkey)) = False Then - If Schema.Complextypes.ContainsKey(vkey.ToLower) AndAlso Schema.Complextypes(vkey.ToLower)("name") <> "CustomValues" Then - Call StoreCP(tgtDataset, etrow, Schema.Complextypes, CP:=vdic(vkey), EntityProperty:=ThisEntityName & ":" & vkey, vkey:=vkey, setid:=SetID, vdic:=vdic) - ElseIf Schema.Complextypes.ContainsKey(vkey.ToLower) AndAlso Schema.Complextypes(vkey.ToLower)("name") = "CustomValues" Then - If etrow.Table.Columns.Contains(vkey) Then etrow.Item(vkey) = json.SerializeObject(vdic(vkey)) - ElseIf Schema.NavProperties.ContainsKey(vkey.ToLower) Then - Call StoreNP(tgtDataset, Schema.Complextypes, Schema.NavProperties(vkey.ToLower), NP:=vdic(vkey), EntityProperty:=ThisEntityName & ":" & vkey, setid:=SetID, vdic:=vdic) - Else - If etrow.Table.Columns.Contains(vkey) Then - If vdic(vkey).GetType() = GetType(System.DateTime) Then - If Year(vdic(vkey)) > 1900 Then etrow.Item(vkey) = vdic(vkey) - ElseIf vdic(vkey).GetType = etrow.Table.Columns(vkey).DataType Then - etrow.Item(vkey) = vdic(vkey) - ElseIf vdic(vkey).GetType = GetType(String) And vdic(vkey).GetType() <> etrow.Table.Columns(vkey).DataType Then - Try - etrow.Item(vkey) = DirectCast(vdic(vkey), System.String).TryConvert(etrow.Table.Columns(vkey).DataType) - Catch cgvex As Exception - Try - etrow.Item(vkey) = vdic(vkey) - Catch cvex As Exception - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "cvex inner " & cvex.Message) - End Try - End Try - Else - Try - etrow.Item(vkey) = vdic(vkey) - Catch cvex As Exception - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "cvex outer " & cvex.Message) - End Try - End If - End If - End If - End If - Next - tgtDataset.Tables(Schema.Entitytablename).Rows.Add(etrow) - tgtDataset.Tables(Schema.Entitytablename).AcceptChanges() - Next - Else - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & " MFR_Response not jArray") - End If - If IsNothing(MFR_Response.nextlink) = False AndAlso MFR_Response.nextlink.ToString <> "" Then - MFR_Response = Await ReadOData(MFR_Response.nextlink.ToString) - Else - MFR_Response = Nothing - End If - - Loop - End If - Next iFilter - - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "Start SQL " & Schema.Entitytablename) - If isProcessed = True Then - For Each tbl As DataTable In tgtDataset.Tables - Dim trc As Integer = tbl.Rows.Count - If trc > 0 OrElse If(EntityID, New Long() {}).Length > 0 Then - Try - tbl.AcceptChanges() - Dim dtwa As New DatatableWriterAsync(tbl, FDSConnectionString) - dtwa.CommandAfterError = New SqlClient.SqlCommand("SELECT * INTO [t_" & SetID.ToLower & "_" & tbl.TableName & "] FROM " & dtwa.DestinationTableName & ";") - - dtwa.CommandAfter = New SqlClient.SqlCommand('"SELECT * INTO [t_" & SetID.ToLower & "_" & tbl.TableName & "] FROM " & dtwa.DestinationTableName & ";" & - "EXECUTE [dbo].[" & (tbl.TableName).Replace("__", "__updt__") & "] @tblname, @referencetable, @tgtid;" & - "EXECUTE [dbo].[fds__setStatus] @table,@action,@setid,@info;" & If(AdditionalCommandAfter, "")) - dtwa.CommandAfter.Parameters.AddRange(New SqlClient.SqlParameter() { - New SqlClient.SqlParameter("@tblname", dtwa.DestinationTableName), - New SqlClient.SqlParameter("@table", tbl.TableName), - New SqlClient.SqlParameter("@action", If(tbl.TableName.ToLower = Schema.Entitytablename.ToLower, "update_", "imdu_") & UpdateNeed.ToString().ToLower()), - New SqlClient.SqlParameter("@setid", SetID.ToLower), - New SqlClient.SqlParameter("@entityname", Schema.ThisEntityName), - New SqlClient.SqlParameter("@info", json.SerializeObject(New With {.count = trc, .reference = Schema.Entitytablename, .id = If(If(EntityID, New Long() {}).Length > 0, vbs.Join(EntityID.Convert(Of String)(Function(x As Long) x.ToString).ToArray, ","), Nothing)})), - New SqlClient.SqlParameter("@referencetable", Schema.Entitytablename), - New SqlClient.SqlParameter("@tgtid", If(If(EntityID, New Long() {}).Length > 0, EntityID(0).ToString, DBNull.Value)) - }) - Diagnostics.Debug.Print("tbs:" & tbl.TableName & " - " & dtwa.DestinationTableName & " - " & SetID.ToLower) - dtwa.DoSubmit() - Catch dsex As Exception - Dlg("Submission issue", dsex.Message, "table: " & tbl.TableName) - End Try - Diagnostics.Debug.Print("OK") - End If - Next - Diagnostics.Debug.Print(Now.ToLongTimeString() & " - " & "End SQL " & Schema.Entitytablename) - End If - End If - Catch ex As Exception - Dlg(ex.Message, "", ex.StackTrace) - Finally - 'release lock - params = New List(Of SqlClient.SqlParameter) From { - New SqlClient.SqlParameter("@tbl", ThisEntityName), - New SqlClient.SqlParameter("@state", False), - New SqlClient.SqlParameter("@override", False), - New SqlClient.SqlParameter("@setid", SetID) - } - Dim Exceptionmessages_gtl As String = "" - 'setSQLValue("EXECUTE [dbo].[ctm__admin_getTableLock] @tbl, @state, @override, @setid;", connectionstring:=FDSConnectionString, SqlParameterList:=params, ExceptionMessage:=Exceptionmessages_gtl) - If DebugDetails = True Then Call DebugToFile("Update__entitytable - locked released for " & ThisEntityName & "(" & SetID & ")", filename:="DebugDetail.txt") - End Try - Else - 'abort action if locked = true - Debug.Print("Updates already locked for " & ThisEntityName & "(" & SetID & ")") - If DebugDetails = True Then Call DebugToFile("Update__entitytable - Updates already locked For " & ThisEntityName & "(" & SetID & ")", filename:="DebugDetail.txt") - End If - Return True - End Function - - - - Private Function StoreCP(ByRef TgtDataset As DataSet, ByRef etrow As DataRow, Complextypes As Dictionary(Of String, Dictionary(Of String, String)), ByVal CP As Object, ByVal EntityProperty As String, ByVal vkey As String, ByVal setid As String, ByVal vdic As Dictionary(Of String, Object)) As Boolean - If CP.GetType = GetType(JArray) Then - For Each vitm As JObject In vdic(vkey) - Call StoreCP(TgtDataset, etrow, Complextypes, CP:=vitm, EntityProperty:=EntityProperty, vkey:=vkey, setid:=setid, vdic:=vdic) - Next - ElseIf CP.GetType = GetType(JObject) Then - Dim CxType As Dictionary(Of String, String) = Complextypes(vkey.ToLower) - Dim cto As Dictionary(Of String, Object) = CP.ToObject(Of Dictionary(Of String, Object)) - Dim cnr As DataRow = TgtDataset.Tables(CxType("tablename")).NewRow - cnr.Item("setid") = setid - For Each cky As String In cto.Keys - If IsNothing(cto(cky)) = False AndAlso cnr.Table.Columns.Contains(cky) Then - 'cnr.Item(cky) = cto(cky) - If cto(cky).GetType() = GetType(System.DateTime) Then - If Year(cto(cky)) > 1900 Then cnr.Item(cky) = cto(cky) - ElseIf cto(cky).GetType = cnr.Table.Columns(cky).DataType Then - cnr.Item(cky) = cto(cky) - ElseIf cto(cky).GetType = GetType(String) And cto(cky).GetType() <> cnr.Table.Columns(cky).DataType Then - Try - cnr.Item(cky) = DirectCast(cto(cky), System.String).TryConvert(cnr.Table.Columns(cky).DataType) - Catch cgvex As Exception - Try - cnr.Item(cky) = cto(cky) - Catch cvex As Exception - End Try - End Try - Else - Try - cnr.Item(cky) = cto(cky) - Catch cvex As Exception - End Try - End If - End If - Next - If cnr.Table.Columns.Contains("EntityId") Then cnr.Item("EntityId") = vdic("Id") - If cnr.Table.Columns.Contains("Property") Then cnr.Item("Property") = EntityProperty - TgtDataset.Tables(CxType("tablename")).Rows.Add(cnr) - If etrow.Table.Columns.Contains(vkey & "#ID") AndAlso cto.ContainsKey("id") Then etrow.Item(vkey & "#ID") = vdic("Id") - End If - - Return True - End Function - - Private Function StoreNP(ByRef TgtDataset As DataSet, Complextypes As Dictionary(Of String, Dictionary(Of String, String)), NavProp As Dictionary(Of String, String), ByVal NP As Object, ByVal EntityProperty As String, ByVal setid As String, ByVal vdic As Dictionary(Of String, Object)) As Boolean - If NP.GetType() = GetType(JArray) Then - For Each vitm As JObject In NP - Call StoreNP(TgtDataset:=TgtDataset, Complextypes:=Complextypes, NavProp:=NavProp, NP:=vitm, EntityProperty:=EntityProperty, setid:=setid, vdic:=vdic) - Next - ElseIf NP.GetType() = GetType(JObject) Then - Dim nto As Dictionary(Of String, Object) = NP.ToObject(Of Dictionary(Of String, Object)) - If nto.ContainsKey("Id") Then - Dim nnr As DataRow = TgtDataset.Tables(NavProp("countertable")).NewRow - Dim ncr As DataRow = TgtDataset.Tables(NavProp("tablename")).NewRow - nnr.Item("setid") = setid - For Each cky As String In nto.Keys - If IsNothing(nto(cky)) = False Then - If Complextypes.ContainsKey(cky.ToLower) AndAlso Complextypes(cky.ToLower)("name") <> "CustomValues" Then - Call StoreCP(TgtDataset:=TgtDataset, etrow:=nnr, Complextypes:=Complextypes, CP:=nto(cky), EntityProperty:=Complextypes(cky.ToLower)("EntityType") & ":" & cky, vkey:=cky, setid:=setid, vdic:=vdic) - Else - If IsNothing(nto(cky)) = False AndAlso nnr.Table.Columns.Contains(cky) Then - 'nnr.Item(cky) = nto(cky) - If nto(cky).GetType() = GetType(System.DateTime) Then - If Year(nto(cky)) > 1900 Then nnr.Item(cky) = nto(cky) - ElseIf nto(cky).GetType = nnr.Table.Columns(cky).DataType Then - nnr.Item(cky) = nto(cky) - ElseIf nto(cky).GetType = GetType(String) And nto(cky).GetType() <> nnr.Table.Columns(cky).DataType Then - Try - nnr.Item(cky) = DirectCast(nto(cky), System.String).TryConvert(nnr.Table.Columns(cky).DataType) - Catch cgvex As Exception - Try - nnr.Item(cky) = nto(cky) - Catch cvex As Exception - End Try - End Try - Else - Try - nnr.Item(cky) = nto(cky) - Catch cvex As Exception - End Try - End If - End If - End If - End If - Next - ncr.Item("EntityId") = vdic("Id") - ncr.Item("PartnerType") = NavProp("countertype") - ncr.Item("PartnerId") = nto("Id") - ncr.Item("Property") = EntityProperty - TgtDataset.Tables(NavProp("tablename")).Rows.Add(ncr) - TgtDataset.Tables(NavProp("countertable")).Rows.Add(nnr) - End If - End If - Return True - End Function - - Private Shared Function newDatatable_SQL(tablename As String) As String - Return System.String.Format("Select TOP(0) [setid] = CAST('' as varchar(50)), * FROM [dbo].[{0}];", tablename) - End Function - Private Async Function newDatatable(tablename As String, setid As String) As Task(Of DataTable) - Dim sqldt As SQLDataTable = Await getSQLDatatable_async(newDatatable_SQL(tablename:=tablename), SqlConnectionString:=FDSConnectionString, options:=New fds_SQLOptions()) - Dim dt As DataTable = sqldt.DataTable.Clone - dt.TableName = tablename - If dt.Columns.Contains("setid") = False Then dt.Columns.Add(New DataColumn("setid", GetType(System.String)) With {.MaxLength = 5}) - dt.Columns("setid").DefaultValue = setid - Return dt - End Function -#Enable Warning BC42356 ' This async method lacks 'Await' operators and so will run synchronously - - Public Async Function Update__Entitytables(Optional DebugDetails As Boolean = False, Optional tgtEntityType As Nullable(Of EntityTypes) = Nothing) As Threading.Tasks.Task - Dim UpdateTasks As New List(Of Threading.Tasks.Task) - - Dim DtF As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) - If DebugDetails = True Then Call DebugToFile("Update__AllEntitytables - " & note & If(If(info, "") <> "", ": " & info, ""), filename:="DebugDetail.txt", exc:=ex, data:=data) - End Sub) - Dim Dlg As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) - Dim str As String = "Update__AllEntitytables - " & note & If(If(info, "") <> "", ": " & info, "") - Call DebugLog(str, data:=If(data, ""), exc:=ex) - If DebugDetails = True Then Call DebugToFile(str, filename:="DebugDetail.txt", exc:=ex, data:=data) - Debug.Print(str) - End Sub) - - Try - 'get updateable tables - 'the SQL query returns all tables that have corresponding temp tables and update procedures - 'a lock status is included to determine if update processes are running - Dim UpdateableTables As SQLDataTable = Await getSQLDatatable_async("SELECT * FROM [dbo].[fds__getUpdateableTables]()" & If(tgtEntityType.HasValue, " WHERE [entity_name] = '" & tgtEntityType.Value.ToString & "'", "") & ";", FDSConnectionString, options:=New fds_SQLOptions()) - Call DtF("UpdateableTables ", UpdateableTables.Exception, "(" & If(UpdateableTables.Count > 0, UpdateableTables.DataTable.Rows.Count.ToString, " no") & " Rows)", Nothing) - - If UpdateableTables.Count > 0 AndAlso UpdateableTables.DataTable.Columns.Contains("updateneed") = True Then 'just to avoid exceptions in next steps - For Each UpdateableTable_rw As DataRow In UpdateableTables.Select("updateneed > 0", "updateneed DESC") - Dim etname As String = UpdateableTable_rw.nz("entity_name", "") - Try 'create capsule for each cycle to prevent everything fails if anything fails - If etname = "" Then - Call DtF("no entity_name received", "", json.SerializeObject(UpdateableTable_rw), Nothing) - ElseIf UpdateableTable_rw.nbool("locked", True) = False Then - Dim Tbl_UpdateNeed As UpdateNeed = UpdateableTable_rw.nint("updateneed", 0) - Dim et As generic.EntityTypes = _generic.EntityValue(etname) - If Not et = EntityTypes.none Then - Call DtF("updating: " & etname, "", json.SerializeObject(UpdateableTable_rw), Nothing) - - Await Update__entitytable(et, UpdateNeed:=Tbl_UpdateNeed, EntityID:=Nothing, DebugDetails:=DebugDetails) - Call DtF("updating task completed", "", "", Nothing) - Else - Call DtF("entitytype not known", "", json.SerializeObject(New With {.etname = etname}), Nothing) - End If - Else - Call DtF("Table update locked" & etname, "", json.SerializeObject(UpdateableTable_rw), Nothing) - End If - Catch tableupdate_ex As Exception - Call Dlg("updatepart failed - " & etname, "", json.SerializeObject(UpdateableTable_rw), tableupdate_ex) - End Try - Next UpdateableTable_rw - End If - Catch exa As Exception - Call Dlg("outer frame", "", "", Nothing) - End Try - End Function - Public Async Function Update__EntityRequests(Optional DebugDetails As Boolean = False) As Threading.Tasks.Task - Dim UpdateTasks As New List(Of Threading.Tasks.Task) - - Dim DtF As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) - If DebugDetails = True Then Call DebugToFile("Update__EntityRequests - " & note & If(If(info, "") <> "", ": " & info, ""), filename:="DebugDetail.txt", exc:=ex, data:=data) - End Sub) - Dim Dlg As New Action(Of String, String, String, Exception)(Sub(note As String, info As String, data As String, ex As Exception) - Dim str As String = "Update__EntityRequests - " & note & If(If(info, "") <> "", ": " & info, "") - Call DebugLog(str, data:=If(data, ""), exc:=ex) - If DebugDetails = True Then Call DebugToFile(str, filename:="DebugDetail.txt", exc:=ex, data:=data) - Debug.Print(str) - End Sub) - - Try - 'get updateable tables - 'the SQL query returns all tables that have corresponding temp tables and update procedures - 'a lock status is included to determine if update processes are running - Dim UpdateableRequests As SQLDataTable = Await getSQLDatatable_async("SELECT * FROM [dbo].[fds__getUpdateableRequests]();", FDSConnectionString, options:=New fds_SQLOptions()) - Call DtF("UpdateableRequests ", UpdateableRequests.Exception, "(" & If(UpdateableRequests.Count > 0, UpdateableRequests.DataTable.Rows.Count.ToString, " no") & " Rows)", Nothing) - - If UpdateableRequests.Count > 0 Then 'just to avoid exceptions in next steps - For Each UpdateableRequest_rw As DataRow In UpdateableRequests.Select("", "order") - Dim etname As String = UpdateableRequest_rw.nz("entity_name", "") - Dim tgtid As Long = UpdateableRequest_rw.nint("Id", -1) - If tgtid > -1 AndAlso etname.IsNullOrWhiteSpace = False Then - Try 'create capsule for each cycle to prevent everything fails if anything fails - Dim et As generic.EntityTypes = _generic.EntityValue(etname) - Await Update__entitytable(et, UpdateNeed:=UpdateNeed.Reset, EntityID:=New Long() {tgtid}, DebugDetails:=DebugDetails, AdditionalCommandAfter:="DELETE FROM [dbo].[fds__mfr_updaterequests] WHERE LOWER([entity_name]) = LOWER(ISNULL(@entityname,'')) AND CAST(ISNULL([Id],'') as varchar(1000)) = @tgtid;") - - Catch tableupdate_ex As Exception - Call Dlg("Update__EntityRequest single failed - " & etname, "", json.SerializeObject(UpdateableRequest_rw), tableupdate_ex) - End Try - End If - Next UpdateableRequest_rw - End If - Catch exa As Exception - Call Dlg("outer frame", "", "", Nothing) - End Try - End Function - - Public Enum PostSubmissionActionMode - full - incremental - reset - End Enum - - - - Public Sub Dispose() Implements IDisposable.Dispose - ''Me.MFRClient.Logout() - DirectCast(Me.MFRClient, IDisposable).Dispose() - End Sub - - - - - End Class - -End Namespace \ No newline at end of file diff --git a/DataService_Legacy/fds_shared.vb b/DataService_Legacy/fds_shared.vb deleted file mode 100644 index c12d49e..0000000 --- a/DataService_Legacy/fds_shared.vb +++ /dev/null @@ -1,226 +0,0 @@ - - -Friend Module fds_shared - - Friend Function SQLConnectionString() As String - Return Configuration.ConfigurationManager.ConnectionStrings("fuchs_ConnectionString").ConnectionString - End Function - Friend Function FDSConnectionString() As String - Return Configuration.ConfigurationManager.ConnectionStrings("fuchs_fds_ConnectionString").ConnectionString - End Function - Friend Function SqlCon() As SqlClient.SqlConnection - Return New SqlClient.SqlConnection(Configuration.ConfigurationManager.ConnectionStrings("fuchs_ConnectionString").ConnectionString) - End Function - - - - Public Function RandomString(rs_length As Byte) As String - Dim r As New Random() - Dim s As String = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" - Dim sb As New Text.StringBuilder - For i As Byte = 1 To rs_length - Dim idx As Integer = r.Next(0, s.Length) - sb.Append(s.Substring(idx, 1)) - Next - Return sb.ToString() - End Function - - - '''' - '''' Returns a delimited containing the field values from a . - '''' - '''' - '''' The input . - '''' - '''' - '''' The delimiter placed between field values. the default value is a comma. - '''' - '''' - '''' A containing the field values from the row separated by the specified delimiter. - '''' - ' - 'Public Function ToCsv(source As DataRow, - ' Optional delimiter As String = ",") As String - ' Return String.Join(delimiter, source.ItemArray) - 'End Function - - ''' - ''' Returns a delimited containing the field values from a . - ''' - ''' - ''' The input . - ''' - ''' - ''' True to wrap values in double-quotes; otherwise, False. - ''' If double-quotes are added, double-quotes within text are escaped with another double-quote. - ''' - ''' - ''' The delimiter placed between field values. the default value is a comma. - ''' - ''' - ''' A containing the field values from the row separated by the specified delimiter. - ''' - - Public Function ToCsv(source As DataRow, - quoteStrings As Boolean, - cultureinfo As Globalization.CultureInfo, - Optional delimiter As String = ",") As String - Dim fieldValues = source.ItemArray - - - Dim rx As New Text.RegularExpressions.Regex("(\"")") - 'Wrap any String values in double-quotes and also escape any double-quotes in the String with another double-quote. - 'replace array by converted array - fieldValues = fieldValues.Select(Function(o) - If IsNothing(o) OrElse IsDBNull(o) Then - Return "" - ElseIf o.GetType = GetType(String) Then - If quoteStrings = True Then - Return Microsoft.VisualBasic.ChrW(34) & rx.Replace(o.ToString, Microsoft.VisualBasic.ChrW(34) & Microsoft.VisualBasic.ChrW(34)) & Microsoft.VisualBasic.ChrW(34) - Else - Return o.ToString - End If - Else - Select Case o.GetType - Case GetType(Decimal) - Return DirectCast(o, Decimal).ToString(cultureinfo) - Case GetType(Single) - Return DirectCast(o, Single).ToString(cultureinfo) - Case GetType(Double) - Return DirectCast(o, Double).ToString(cultureinfo) - Case GetType(Boolean) - Return DirectCast(o, Boolean).ToString(cultureinfo) - Case GetType(System.DateTime) - Return DirectCast(o, DateTime).ToUniversalTime.ToString("U") - Case Else - Return o.ToString() - End Select - End If - End Function).ToArray() - - - Return String.Join(delimiter, fieldValues) - End Function - - - - ''' - ''' Returns a delimited containing the field values from the rows a . - ''' - ''' The input . - ''' True to include a row of column headers; otherwise, False - ''' True to wrap values in double-quotes; otherwise, False. - ''' If double-quotes are added, double-quotes within text are escaped with another double-quote. - ''' The delimiter placed between rows. the default value is a line break comprising a carriage return and a line feed. - ''' The delimiter placed between field values. the default value is a comma. - ''' The culture that is used to convert float-point numbers like or to string.
This falls back to InvariantCulture, if not provided. - ''' True to wrap column header names in double-quotes; otherwise, False.
- ''' If no value is provided, the settings falls back to quoteStrings parameter. - ''' A containing the field values from the rows of the table separated by the specified delimiters. - - Public Function ToCsv(source As DataTable, - includeHeaders As Boolean, - quoteStrings As Boolean, - Optional rowDelimiter As String = ControlChars.CrLf, - Optional fieldDelimiter As String = ",", - Optional cultureinfo As Globalization.CultureInfo = Nothing, - Optional quoteHeader As Boolean? = Nothing) As String - If quoteHeader.HasValue = False Then quoteHeader = quoteStrings - cultureinfo = If(cultureinfo, Globalization.CultureInfo.InvariantCulture) 'fallback if not provided - Dim rows = source.Rows. - Cast(Of DataRow)(). - Select(Function(row) row.ToCsv(quoteStrings:=quoteStrings, cultureinfo:=cultureinfo, delimiter:=fieldDelimiter)) - - If includeHeaders = True Then - Dim rx As New Text.RegularExpressions.Regex("(\"")") - Dim headers = String.Join(fieldDelimiter, - source.Columns. - Cast(Of DataColumn)(). - Select(Function(column) If(quoteHeader.Value, - Microsoft.VisualBasic.ChrW(34) & rx.Replace(column.ColumnName.ToString, Microsoft.VisualBasic.ChrW(34) & Microsoft.VisualBasic.ChrW(34)) & Microsoft.VisualBasic.ChrW(34), - column.ColumnName))) - - rows = {headers}.Concat(rows) - End If - - Return String.Join(rowDelimiter, rows) - End Function - - ''' - ''' Returns a text-file containing the string, created by streamwriter. - ''' - ''' The input . - ''' The encoding used with streamwriter for the textfile. This falls back to , if not provided. - ''' A file as byte-array. - - Public Function ToByteArray(input As String, Optional encoding As System.Text.Encoding = Nothing) As Byte() - Dim content As Byte() = Nothing - Using ms As New IO.MemoryStream - Using sw As New IO.StreamWriter(ms, encoding:=If(encoding, System.Text.Encoding.UTF8)) - sw.Write(input) - sw.Flush() - ms.Position = 0 - content = ms.ToArray() - End Using - End Using - Return content - End Function - - - Public Function WriteStreamToDisk(ByVal StreamToWrite As IO.Stream, ByVal FilePath As String) As Boolean - 'Dim tmpFilePath As String = Left(FilePath, Len(FilePath) - 4) & ".tmp" - Dim cnt = 0 -restart: - Try - If My.Computer.FileSystem.FileExists(FilePath) Then My.Computer.FileSystem.DeleteFile(FilePath) - Using FleStream As System.IO.FileStream = New System.IO.FileStream(FilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.Delete) - ReadWriteStream(StreamToWrite, FleStream, True) - End Using - Catch ex As Exception - System.Diagnostics.Debug.WriteLine($"{"WriteStreamToDisk - " & ex.Message}") - cnt += 1 - If cnt = 6 Then - Return False - Exit Function - Else - Threading.Thread.Sleep(500) - GoTo restart - End If - End Try - Return True - End Function - - Public Function ReadWriteStream(ByVal readStream As IO.Stream, ByVal writeStream As IO.Stream, ByVal closeWriteStream As Boolean) As Boolean - Try - Dim Length As Integer = 256 - Dim buffer(Length - 1) As Byte - readStream.Seek(0, System.IO.SeekOrigin.Begin) - Dim bytesRead As Integer = readStream.Read(buffer, 0, Length) - 'write the required bytes - While (bytesRead > 0) - writeStream.Write(buffer, 0, bytesRead) - bytesRead = readStream.Read(buffer, 0, Length) - End While - readStream.Close() - If closeWriteStream = True Then writeStream.Close() - Return True - Catch ex As Exception - System.Diagnostics.Debug.WriteLine($"{"ReadWriteStream - " & ex.Message}") - Call OCMS.debug_log("files_folders ReadWriteStream", ex) - Return False - End Try - End Function - - - - Public Function NameBase(ByVal FI As System.IO.FileInfo) As String - Return FI.Name.Substring(startIndex:=0, length:=FI.Name.Length - FI.Extension.Length) - End Function - - - - - Public Function MimeType(ByVal FI As System.IO.FileInfo) As String - Return System.Web.MimeMapping.GetMimeMapping(FI.Name) - End Function -End Module diff --git a/DataService_Legacy/fds_zip.vb b/DataService_Legacy/fds_zip.vb deleted file mode 100644 index 523953f..0000000 --- a/DataService_Legacy/fds_zip.vb +++ /dev/null @@ -1,500 +0,0 @@ -Imports SevenZip 'Squid-Box.SevenZipSharp -Imports System.IO - -Namespace Global.fds - Public Class Archive - Implements IDisposable - - 'Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long - 'Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) - - 'Public Enum Timeunit As Long - ' Milliseconds = 0 - ' Seconds = 1000 - ' Minutes = 60000 - 'End Enum - 'Public Sub Wait(ByVal No As Integer, ByVal unit As Timeunit) - ' Dim tme As Long = CLng(No * unit) - ' Sleep(tme) - 'End Sub - - Public Event Saving() - Public Event FileSaved() - Public Event FileStreamCreated() - - Private _ArchiveFile As FileInfo - Private _ArchivePassword As String - Private _ArchiveFormat As OutArchiveFormat - Public TempPath As String = System.AppDomain.CurrentDomain.BaseDirectory - Public Property ArchiveFileStream As IO.Stream - - Private ZipOut As SevenZipExtractor - Private ZipIn As SevenZipCompressor - Public ZipAppend As Boolean = True - - Public ExitOK As Boolean = False - Public ZipInOK As Boolean = False - - Public Sub New(ByVal ArchiveFile As FileInfo, Optional ByVal ArchivePassword As String = "", Optional ByVal INIT As Boolean = True, Optional ByVal Type As OutArchiveFormat = OutArchiveFormat.SevenZip) - Me._ArchiveFormat = Type - Me._ArchiveFile = New FileInfo(ArchiveFile.FullName.Replace(ArchiveFile.Extension, If(Type = OutArchiveFormat.SevenZip, ".7z", ArchiveFile.Extension))) - Me._ArchivePassword = ArchivePassword - If INIT = True Then Call InitZipIn(Type) - End Sub - - Private Sub InitZipIn(ByVal Type As OutArchiveFormat) - Dim assemblydirectory As IO.DirectoryInfo - If Zipping.SevenZipPath = "" Then - Try - assemblydirectory = New IO.DirectoryInfo(New Uri(System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase)).LocalPath) - Dim zip As IO.FileInfo = assemblydirectory.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault - Zipping.SevenZipPath = If(IsNothing(zip), "", zip.FullName) - Finally - If Zipping.SevenZipPath = "" Then - assemblydirectory = New IO.DirectoryInfo(System.AppDomain.CurrentDomain.BaseDirectory) - Dim zip As IO.FileInfo = assemblydirectory.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault - Zipping.SevenZipPath = If(IsNothing(zip), "", zip.FullName) - End If - End Try - If Zipping.SevenZipPath = "" Then - OCMS.debug_log("DDA.intranet.Zipping Archive InitZipIn", error:="SevenZipPath not found") - End If - End If - SevenZipCompressor.SetLibraryPath(SevenZipPath) - - Me.ZipIn = New SevenZipCompressor - With Me.ZipIn - If Type = OutArchiveFormat.SevenZip AndAlso Me._ArchiveFile.Extension.ToLower.Contains("7z") = True Then - .ArchiveFormat = OutArchiveFormat.SevenZip - Else - .ArchiveFormat = Type - End If - - .CompressionLevel = SevenZip.CompressionLevel.Ultra - Select Case .ArchiveFormat - Case OutArchiveFormat.SevenZip - .CompressionMethod = SevenZip.CompressionMethod.Lzma2 - Case OutArchiveFormat.Zip, OutArchiveFormat.GZip - .CompressionMethod = CompressionMethod.Deflate - Case Else - .CompressionMethod = CompressionMethod.Default - End Select - - If ZipAppend = True Then - .CompressionMode = SevenZip.CompressionMode.Append - Else - .CompressionMode = SevenZip.CompressionMode.Create - End If - .DirectoryStructure = False - End With - Me.ZipInOK = True - End Sub - - Public Sub Extract(ByVal DataArchiveFilePath As FileInfo, ByVal TgtDirectory As DirectoryInfo, Optional ByVal Type As OutArchiveFormat = Nothing) - If DataArchiveFilePath.Exists Then - If IsNothing(Type) = True AndAlso DataArchiveFilePath.Extension.ToLower.Contains("7z") = True Then - Type = OutArchiveFormat.SevenZip - ElseIf IsNothing(Type) = True Then - Type = OutArchiveFormat.Zip - End If - - If Me.ZipInOK = False Then Call InitZipIn(Type) - - If Me._ArchivePassword = "" Then - Me.ZipOut = New SevenZipExtractor(DataArchiveFilePath.FullName) - Else - Me.ZipOut = New SevenZipExtractor(DataArchiveFilePath.FullName, Me._ArchivePassword) - End If - - Try - If Me.ZipOut.ArchiveFileData(0).Encrypted = False And Not Me._ArchivePassword = "" Then - Me._ArchivePassword = "" - End If - Catch ex As Exception - System.Diagnostics.Debug.WriteLine($"{"zip Extract - " & ex.Message}") - OCMS.debug_log("DDA.intranet.Zipping Archive InitZipIn", ex:=ex, data:=New With {.DataArchiveFilePath = DataArchiveFilePath.FullName, .TgtDirectory = TgtDirectory.FullName}) - Exit Sub - End Try - - If Not ZipOut Is Nothing Then - Me.ZipOut.ExtractArchive(TgtDirectory.FullName) - Me.ZipOut.Dispose() - End If - End If - End Sub - - Public Function FileInfo_to_Filepaths_Converter() As Converter(Of FileInfo, String) - Return New Converter(Of FileInfo, String)(Function(filepath As FileInfo) filepath.FullName()) - End Function - Public Function Filepaths_to_FileInfo_Converter() As Converter(Of String, FileInfo) - Return New Converter(Of String, FileInfo)(Function(filepath As String) New FileInfo(filepath)) - End Function - - Public Function Compress(ByVal FilePaths As List(Of String), Optional ByVal ArchiveFilePath As String = Nothing, Optional ByVal ArchivePass As String = Nothing, Optional ByVal Type As OutArchiveFormat = OutArchiveFormat.SevenZip) As Boolean - Return Compress(Files:=FilePaths.ConvertAll(Filepaths_to_FileInfo_Converter()), ArchiveFile:=If(IsNothing(ArchiveFilePath), Nothing, New FileInfo(ArchiveFilePath)), ArchivePass:=ArchivePass, Type:=Type) - End Function - Public Function Compress(ByVal Files As List(Of FileInfo), Optional ByVal ArchiveFile As FileInfo = Nothing, Optional ByVal ArchivePass As String = Nothing, Optional ByVal Type As OutArchiveFormat = OutArchiveFormat.SevenZip) As Boolean - If Files.Count = 0 Then Return True - If Me.ZipInOK = False Then Call InitZipIn(Type) - - If IsNothing(ArchiveFile) = True Then ArchiveFile = Me._ArchiveFile - If If(IsNothing(ArchivePass), "", ArchivePass) = "" Then ArchivePass = Me._ArchivePassword - - If ArchiveFile.Exists() AndAlso Me.ZipAppend = True Then - Me.ZipIn.CompressionMode = CompressionMode.Append - Else - If ArchiveFile.Exists = True Then ArchiveFile.Delete() - Me.ZipIn.CompressionMode = CompressionMode.Create - End If - - Try - Dim FilesVerified As FileInfo() = Files.Where(Function(f As FileInfo) f.Exists).ToArray() - Dim FilePaths As String() = Array.ConvertAll(Of FileInfo, String)(FilesVerified, FileInfo_to_Filepaths_Converter()) - If ArchivePass = "" Then - Me.ZipIn.CompressFiles(ArchiveFile.FullName, FilePaths) - Else - Me.ZipIn.EncryptHeaders = True - Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256 - Me.ZipIn.CompressFilesEncrypted(ArchiveFile.FullName, ArchivePass, FilePaths) - End If - RaiseEvent FileSaved() - - 'Debug.Print("Saved: " & Now().ToString) - Me.ExitOK = True - 'Disposing... - Me.ZipIn = Nothing - Me.ZipInOK = False - Catch ex As Exception - 'Debug.Print("NOT Saved: " & Now().ToString) - Me.ExitOK = False - End Try - Return Me.ExitOK AndAlso ArchiveFile.Exists - End Function - Public Function Compress(ByVal FilePath As String) As Boolean - Dim FL As New List(Of String) From { - FilePath - } - Call Compress(FL) - Return True - End Function - Public Function CompressToStream(ByVal FilePath As String) As Boolean - Dim FL As New List(Of String) From { - FilePath - } - Call CompressToStream(FL) - Return True - End Function - - Public Function CompressToStream(ByVal FilePaths As List(Of String)) As Boolean - Return CompressToStream(Files:=FilePaths.ConvertAll(Filepaths_to_FileInfo_Converter())) - End Function - Public Function CompressToStream(ByVal Files As List(Of FileInfo)) As Boolean - If Files.Count = 0 Then Return True - If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat) - - If IsNothing(_ArchiveFileStream) Then 'nur wenn der interne leer ist... - Me.ZipIn.CompressionMode = CompressionMode.Create - Me._ArchiveFileStream = New MemoryStream - Else - Me.ZipIn.CompressionMode = CompressionMode.Append - End If - - Try - Dim FilesVerified As FileInfo() = Files.Where(Function(f As FileInfo) f.Exists).ToArray() - Dim FilePaths As String() = Array.ConvertAll(Of FileInfo, String)(FilesVerified, FileInfo_to_Filepaths_Converter()) - If Me._ArchivePassword = "" Then - Me.ZipIn.CompressFiles(Me._ArchiveFileStream, FilePaths) - Else - Me.ZipIn.EncryptHeaders = True - Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256 - Me.ZipIn.CompressFilesEncrypted(Me._ArchiveFileStream, Me._ArchivePassword, FilePaths) - End If - Me._ArchiveFileStream.Seek(0, SeekOrigin.Begin) - RaiseEvent FileStreamCreated() - - 'Debug.Print("Saved: " & Now().ToString) - Me.ExitOK = True - 'Disposing... - Me.ZipIn = Nothing - Me.ZipInOK = False - Catch ex As Exception - 'Debug.Print("NOT Saved: " & Now().ToString) - Me.ExitOK = False - End Try - Return Me.ExitOK - End Function - - Public Function CompressToStream(ByVal Files As List(Of FileInfo), ByRef TargetStream As IO.Stream) As Boolean - If Files.Count = 0 Then Return True - If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat) - - If IsNothing(TargetStream) = True Then - TargetStream = New MemoryStream - End If - Me.ZipIn.CompressionMode = CompressionMode.Create - Dim FilePaths As String() = New String() {} - Try - Dim FilesVerified As FileInfo() = Files.Where(Function(f As FileInfo) f.Exists).ToArray() - FilePaths = Array.ConvertAll(Of FileInfo, String)(FilesVerified, FileInfo_to_Filepaths_Converter()) - If Me._ArchivePassword = "" Then - Me.ZipIn.CompressFiles(TargetStream, FilePaths) - Else - Me.ZipIn.EncryptHeaders = True - Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256 - Me.ZipIn.CompressFilesEncrypted(TargetStream, Me._ArchivePassword, FilePaths) - End If - TargetStream.Seek(0, SeekOrigin.Begin) - - 'Debug.Print("Saved: " & Now().ToString) - Me.ExitOK = True - 'Disposing... - Me.ZipIn = Nothing - Me.ZipInOK = False - Catch ex As Exception - 'Debug.Print("NOT Saved: " & Now().ToString) - OCMS.debug_log("IntranetController zip", ex, data:=New With {.filepaths = FilePaths}) - Me.ExitOK = False - End Try - Return Me.ExitOK - End Function - - - Public Function CompressToStream(ByVal Files As Dictionary(Of String, IO.Stream)) As Boolean - If Files.Count = 0 Then Return True - If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat) - - If IsNothing(_ArchiveFileStream) Then 'nur wenn der interne leer ist... - Me.ZipIn.CompressionMode = CompressionMode.Create - Me._ArchiveFileStream = New MemoryStream - Else - Me.ZipIn.CompressionMode = CompressionMode.Append - End If - - Try - If Me._ArchivePassword = "" Then - Me.ZipIn.CompressStreamDictionary(streamDictionary:=Files, Me._ArchiveFileStream) - Else - Me.ZipIn.EncryptHeaders = True - Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256 - Me.ZipIn.CompressStreamDictionary(streamDictionary:=Files, Me._ArchiveFileStream, password:=Me._ArchivePassword) - End If - Me._ArchiveFileStream.Seek(0, SeekOrigin.Begin) - RaiseEvent FileStreamCreated() - - 'Debug.Print("Saved: " & Now().ToString) - Me.ExitOK = True - 'Disposing... - Me.ZipIn = Nothing - Me.ZipInOK = False - Catch ex As Exception - 'Debug.Print("NOT Saved: " & Now().ToString) - Me.ExitOK = False - End Try - Return Me.ExitOK - End Function - Public Function CompressToStream(ByVal Files As Dictionary(Of String, Byte()), Optional targetstream As IO.Stream = Nothing) As Boolean - If Files.Count = 0 Then Return True - If Me.ZipInOK = False Then Call InitZipIn(Me._ArchiveFormat) - - If IsNothing(_ArchiveFileStream) Then 'nur wenn der interne leer ist... - Me.ZipIn.CompressionMode = CompressionMode.Create - Me._ArchiveFileStream = New MemoryStream - Else - Me.ZipIn.CompressionMode = CompressionMode.Append - End If - - Try - Dim FilesStreams As New Dictionary(Of String, IO.Stream) - For Each fy As String In Files.Keys - FilesStreams.Add(fy, New IO.MemoryStream(Files(fy))) - Next - - If Me._ArchivePassword = "" Then - Me.ZipIn.CompressStreamDictionary(streamDictionary:=FilesStreams, If(IsNothing(targetstream), Me._ArchiveFileStream, targetstream)) - Else - Me.ZipIn.EncryptHeaders = True - Me.ZipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256 - Me.ZipIn.CompressStreamDictionary(streamDictionary:=FilesStreams, If(IsNothing(targetstream), Me._ArchiveFileStream, targetstream), password:=Me._ArchivePassword) - End If - Me._ArchiveFileStream.Seek(0, SeekOrigin.Begin) - RaiseEvent FileStreamCreated() - - 'Debug.Print("Saved: " & Now().ToString) - Me.ExitOK = True - 'Disposing... - Me.ZipIn = Nothing - Me.ZipInOK = False - Catch ex As Exception - 'Debug.Print("NOT Saved: " & Now().ToString) - Me.ExitOK = False - End Try - Return Me.ExitOK - End Function - - - Public Function WriteArchiveStreamToDisk(Optional ByVal ArchiveFile As IO.FileInfo = Nothing) As Boolean - Try - If Me._ArchiveFile.Exists() Then Me._ArchiveFile.Delete() - Catch ex As Exception - End Try - If IsNothing(ArchiveFile) = False Then 'Wenn ein DateiPfad hier übergeben wurde... - WriteStreamToDisk(Me._ArchiveFileStream, ArchiveFile.FullName) - Else ' sonst wird der interne genommen - WriteStreamToDisk(Me._ArchiveFileStream, Me._ArchiveFile.FullName) - End If - Return Me._ArchiveFile.Exists - End Function - - - - 'Private ArchiveMail As EMail = Nothing - 'Public Function SendZip(Optional ByVal Subject As String = Nothing, Optional ByVal BodyText As String = Nothing) As Boolean - ' Try - ' If ArchiveMail Is Nothing Then Exit Function - ' If Not Subject = Nothing Then - ' ArchiveMail.Subject = Subject - ' End If - ' If Not BodyText = Nothing Then - ' ArchiveMail.Body = BodyText - ' End If - ' 'Send with archive as attachment - ' Return ArchiveMail.Send(_ArchivePath.Path) - ' Catch ex As Exception - ' Return False - ' End Try - 'End Function - 'Public Function SendZipStream(Optional ByVal Subject As String = Nothing, Optional ByVal BodyText As String = Nothing) As Boolean - ' Try - ' If ArchiveMail Is Nothing Then Exit Function - ' If Not Subject = Nothing Then - ' ArchiveMail.Subject = Subject - ' End If - ' If Not BodyText = Nothing Then - ' ArchiveMail.Body = BodyText - ' End If - ' 'Send with archive as attachment - ' Return ArchiveMail.Send(_ArchivePath.Name, _ArchiveFileStream) - ' Catch ex As Exception - ' Return False - ' End Try - 'End Function - 'Public Sub SetMailSettings(ByVal SMTP As MailServer_Settings, ByVal MAIL As Mail_Settings) - ' ArchiveMail = New EMail(SMTP, MAIL) - 'End Sub - - -#Region "IDisposable Support" - Private disposedValue As Boolean ' To detect redundant calls - - ' IDisposable - Protected Overridable Sub Dispose(disposing As Boolean) - If Not disposedValue Then - If disposing Then - ' TODO: dispose managed state (managed objects). - Try - If IsNothing(Me._ArchiveFileStream) = False Then Me._ArchiveFileStream.Dispose() - If IsNothing(Me.ZipOut) = False Then Me.ZipOut.Dispose() - Me.ZipIn = Nothing - Catch ex As Exception - - End Try - End If - - ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below. - ' TODO: set large fields to null. - End If - disposedValue = True - End Sub - - ' TODO: override Finalize() only if Dispose(disposing As Boolean) above has code to free unmanaged resources. - 'Protected Overrides Sub Finalize() - ' ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above. - ' Dispose(False) - ' MyBase.Finalize() - 'End Sub - - ' This code added by Visual Basic to correctly implement the disposable pattern. - Public Sub Dispose() Implements IDisposable.Dispose - ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above. - Dispose(True) - ' TODO: uncomment the following line if Finalize() is overridden above. - ' GC.SuppressFinalize(Me) - End Sub -#End Region - - - End Class - - - Public Module Zipping - - Public SevenZipPath As String = "" - - Public Sub FastAppend(ByVal FileToZip As FileInfo, ByVal ArchiveFile As FileInfo) - If FileToZip.Exists AndAlso IsNothing(ArchiveFile) = False AndAlso ArchiveFile.Exists Then - Dim Zip As New Archive(ArchiveFile) With { - .ZipAppend = True - } - Dim FL As New List(Of String) From { - FileToZip.FullName - } - Zip.Compress(FL) - End If - End Sub - Public Sub FastAppend(ByVal FileToZip As FileInfo, ByVal TgtArchiveDirectory As DirectoryInfo, ByVal ArchiveName As String) - Dim ArchiveFile As New FileInfo(TgtArchiveDirectory.FullName & If(Strings.Right(TgtArchiveDirectory.FullName, 1) = "\", "", "\") & ArchiveName) - If ArchiveFile.Exists Then Call FastAppend(FileToZip, ArchiveFile:=ArchiveFile) - End Sub - Public Sub FastAppend(ByVal FileToZip As FileInfo, ByVal TgtArchiveDirectoryPath As String, ByVal ArchiveName As String) - Dim ArchiveFile As New FileInfo(TgtArchiveDirectoryPath & If(Strings.Right(TgtArchiveDirectoryPath, 1) = "\", "", "\") & ArchiveName) - If ArchiveFile.Exists Then Call FastAppend(FileToZip, ArchiveFile:=ArchiveFile) - End Sub - Public Function FastZip(ByVal DirectoryToZip As DirectoryInfo, Optional ByVal Append As Boolean = True) As IO.FileInfo - If DirectoryToZip.Exists Then - Dim ZipFile As New FileInfo(DirectoryToZip.Name + ".7z"), cnt As Integer = 0 - If Append = False Then - Do Until ZipFile.Exists = False - cnt += 1 - ZipFile = New FileInfo(DirectoryToZip.Name & "_" & CStr(cnt) & ".7z") - Loop - End If - Dim Zip As New Archive(ZipFile) With { - .ZipAppend = Append - } - Zip.Compress(New List(Of FileInfo)(DirectoryToZip.GetFiles)) - Return If(ZipFile.Exists, ZipFile, Nothing) - Else - Return Nothing - End If - End Function - Public Function FastZip(ByVal FileToZip As IO.FileInfo, Optional ByVal Append As Boolean = True) As IO.FileInfo - If FileToZip.Exists Then - Dim ZipFile As New FileInfo(FileToZip.Name.Replace("." & FileToZip.Extension, "") + ".7z"), cnt As Integer = 0 - If Append = False Then - Do Until ZipFile.Exists = False - cnt += 1 - ZipFile = New FileInfo(FileToZip.Name.Replace("." & FileToZip.Extension, "") & "_" & CStr(cnt) & ".7z") - Loop - End If - Dim Zip As New Archive(ZipFile) With { - .ZipAppend = Append - } - Dim FL As New List(Of FileInfo) From { - FileToZip - } - Zip.Compress(FL) - Return If(ZipFile.Exists, ZipFile, Nothing) - Else - Return Nothing - End If - End Function - Public Function FastZip(ByVal TgtDirectory As String, ByVal Filename As String, Optional ByVal Append As Boolean = True) As IO.FileInfo - Return FastZip(New FileInfo(TgtDirectory & If(Strings.Right(TgtDirectory, 1) = "\", "", "\") & Filename), Append) - End Function - - - - - End Module - -End Namespace diff --git a/DataService_Legacy/install.bat b/DataService_Legacy/install.bat deleted file mode 100644 index bd9c444..0000000 --- a/DataService_Legacy/install.bat +++ /dev/null @@ -1 +0,0 @@ -Fuchs_Dataservice.exe install --autostart \ No newline at end of file diff --git a/DataService_Legacy/packages.config b/DataService_Legacy/packages.config deleted file mode 100644 index 3823bf2..0000000 --- a/DataService_Legacy/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/DataService_Legacy/un-install.bat b/DataService_Legacy/un-install.bat deleted file mode 100644 index 1312ccc..0000000 --- a/DataService_Legacy/un-install.bat +++ /dev/null @@ -1 +0,0 @@ -Fuchs_Dataservice.exe uninstall \ No newline at end of file diff --git a/Fuchs.Tests/DocumentMetadataBuilderTests.cs b/Fuchs.Tests/DocumentMetadataBuilderTests.cs index 7cd869b..357a24c 100644 --- a/Fuchs.Tests/DocumentMetadataBuilderTests.cs +++ b/Fuchs.Tests/DocumentMetadataBuilderTests.cs @@ -110,6 +110,20 @@ public class DocumentMetadataBuilderTests Assert.Equal(guid.ToString(), metadata["file_guid"]); } + [Theory] + [InlineData("Zahlungserinnerung für Müller", "Zahlungserinnerung f%C3%BCr M%C3%BCller")] + [InlineData("Rechnung\r\nKunde", "Rechnung%0D%0AKunde")] + public void Build_NonAsciiOrControlCharacters_PercentEncodesUtf8ForHttpHeaders( + string value, string expected) + { + var row = new Dictionary { ["DocumentName"] = value }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "DocumentName" }); + + Assert.Equal(expected, metadata["DocumentName"]); + Assert.All(metadata["DocumentName"], c => Assert.InRange(c, '\u0020', '\u007e')); + } + // ── Field-list edge cases ──────────────────────────────────────────────── [Theory] [InlineData("")] diff --git a/Fuchs.Tests/FdsDataServiceTests.cs b/Fuchs.Tests/FdsDataServiceTests.cs new file mode 100644 index 0000000..e4ea278 --- /dev/null +++ b/Fuchs.Tests/FdsDataServiceTests.cs @@ -0,0 +1,446 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using fds; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Fuchs.Tests; + +// ───────────────────────────────────────────────────────────────────────────── +// Tests for the Fuchs_DataService library (MFR ERP sync). +// +// Since Topshelf/own-config were removed and the library is hosted in-process by +// Fuchs, these tests exercise the parts that are pure/deterministic and do not +// require a live SQL Server or MFR endpoint: DATEV header/CSV/XML formatting, +// UpdateNeed parsing, the FdsShared utility helpers, config resolution, and the +// (network-free) FdsMfrClient construction. +// ───────────────────────────────────────────────────────────────────────────── + +/// DATEV header line formatting — pure string assembly, no I/O. +public class DatevHeaderTests +{ + [Fact] + public void ToHeaderString_Defaults_ProducesThirtySemicolonFields() + { + var header = new DatevHeader(); + var parts = header.ToHeaderString().Split(';'); + + Assert.Equal(30, parts.Length); + Assert.Equal("\"EXTF\"", parts[0]); // Kennzeichen quoted + Assert.Equal("700", parts[1]); // Versionsnummer + Assert.Equal("1", parts[20]); // Festschreibung true → "1" + Assert.Equal("\"EUR\"", parts[21]); // WKZ quoted + } + + [Fact] + public void ToHeaderString_MapsNumericAndDateFields() + { + var header = new DatevHeader + { + Beraternummer = 12345, + Mandantennummer = 678, + Sachkontenlänge = 4, + WJBeginn = new DateTime(2026, 1, 1), + DatumVon = new DateTime(2026, 7, 1), + DatumBis = new DateTime(2026, 7, 31), + Bezeichnung = "fds_m260731", + }; + var parts = header.ToHeaderString().Split(';'); + + Assert.Equal("12345", parts[10]); + Assert.Equal("678", parts[11]); + Assert.Equal("20260101", parts[12]); + Assert.Equal("4", parts[13]); + Assert.Equal("20260701", parts[14]); + Assert.Equal("20260731", parts[15]); + Assert.Equal("\"fds_m260731\"", parts[16]); + } + + [Theory] + [InlineData(true, "1")] + [InlineData(false, "0")] + public void ToHeaderString_Festschreibung_MapsToFlag(bool festschreibung, string expected) + { + var header = new DatevHeader { Festschreibung = festschreibung }; + Assert.Equal(expected, header.ToHeaderString().Split(';')[20]); + } + + [Fact] + public void ToHeaderString_DtvfKennzeichen_IsHonoured() + { + var header = new DatevHeader { Kennzeichen = DatevKennzeichen.DTVF }; + Assert.Equal("\"DTVF\"", header.ToHeaderString().Split(';')[0]); + } + + [Theory] + [InlineData(DatevFormatkategorie.Debitoren__Kreditoren, "Debitoren/Kreditoren")] + [InlineData(DatevFormatkategorie.Buchungsstapel, "Buchungsstapel")] + [InlineData(DatevFormatkategorie.Wiederkehrende_Buchungen, "Wiederkehrende Buchungen")] + public void Formatname_TranslatesEnumUnderscores(DatevFormatkategorie kat, string expected) + { + var header = new DatevHeader { Formatkategorie = kat }; + Assert.Equal(expected, header.Formatname); + // Field 3 embeds the same, quoted. + Assert.Equal($"\"{expected}\"", header.ToHeaderString().Split(';')[3]); + } +} + +/// DATEV CSV + document XML generation on FdsMfr (no DB / no MFR). +public class FdsMfrDatevTests +{ + private static FdsMfr NewMfr() => + new(NullLogger.Instance, NullLoggerFactory.Instance); + + [Fact] + public void DATEV_PrependsHeaderLine_AndSemicolonCsvWithColumnHeaders() + { + var header = new DatevHeader { Bezeichnung = "test" }; + var tbl = new DataTable(); + tbl.Columns.Add("Konto", typeof(string)); + tbl.Columns.Add("Betrag", typeof(decimal)); + tbl.Rows.Add("1200", 119.50m); + + var result = NewMfr().DATEV(header, tbl); + var lines = result.Split("\r\n"); + + Assert.Equal(header.ToHeaderString(), lines[0]); // header line first + Assert.Equal("Konto;Betrag", lines[1]); // column headers, semicolon-delimited, unquoted + Assert.Contains("1200", lines[2]); + // de-DE culture → decimal comma + Assert.Contains("119,5", lines[2]); + } + + // Note: only the root is in the DATEV namespace; the generator emits child + // elements in the empty namespace (existing production behavior). Child lookups therefore + // use namespace-agnostic local-name() XPath rather than the datev namespace prefix. + + [Fact] + public void CreateDatevDocumentXml_BuildsArchiveWithNamespaceAndOneDocumentPerInput() + { + var docs = new List + { + new("guid-1", "invoice1.pdf", "RgNr: 100", "ProcessWeb_Belege", "2026/07"), + new("guid-2", "invoice2.pdf", "", "ProcessWeb_Belege", "2026/07"), + }; + + var xml = NewMfr().CreateDatevDocumentXml(docs); + var doc = new XmlDocument(); + doc.LoadXml(xml); + + Assert.Equal("archive", doc.DocumentElement!.LocalName); + Assert.Equal("http://xml.datev.de/bedi/tps/document/v04.0", doc.DocumentElement.NamespaceURI); + Assert.Equal("ProcessWeb", doc.DocumentElement.GetAttribute("generatingSystem")); + Assert.Equal("4.0", doc.DocumentElement.GetAttribute("version")); + + Assert.NotNull(doc.SelectSingleNode("//*[local-name()='header']/*[local-name()='date']")); + Assert.NotNull(doc.SelectSingleNode("//*[local-name()='header']/*[local-name()='description']")); + + var documentNodes = doc.SelectNodes("//*[local-name()='content']/*[local-name()='document']"); + Assert.Equal(2, documentNodes!.Count); + } + + [Fact] + public void CreateDatevDocumentXml_OmitsKeywordsWhenEmpty_IncludesWhenPresent() + { + var docs = new List + { + new("g-withkw", "a.pdf", "keyword-here", "ProcessWeb_Belege", "2026/07"), + new("g-nokw", "b.pdf", "", "ProcessWeb_Belege", "2026/07"), + }; + + var doc = new XmlDocument(); + doc.LoadXml(NewMfr().CreateDatevDocumentXml(docs)); + + var withKw = doc.SelectSingleNode("//*[local-name()='document'][@guid='g-withkw']")!; + var noKw = doc.SelectSingleNode("//*[local-name()='document'][@guid='g-nokw']")!; + + var withKwNode = withKw.SelectSingleNode("*[local-name()='keywords']"); + Assert.NotNull(withKwNode); + Assert.Equal("keyword-here", withKwNode.InnerText); + Assert.Null(noKw.SelectSingleNode("*[local-name()='keywords']")); + } + + [Fact] + public void CreateDatevDocumentXml_EmitsThreeRepositoryLevels() + { + var docs = new List + { + new("g1", "a.pdf", "", "ProcessWeb_Belege", "2026/07_w03"), + }; + var doc = new XmlDocument(); + doc.LoadXml(NewMfr().CreateDatevDocumentXml(docs)); + + var levels = doc.SelectNodes( + "//*[local-name()='document']/*[local-name()='repository']/*[local-name()='level']")!; + Assert.Equal(3, levels.Count); + Assert.Equal("ProcessWeb", ((XmlElement)levels[0]!).GetAttribute("name")); + Assert.Equal("ProcessWeb_Belege", ((XmlElement)levels[1]!).GetAttribute("name")); + Assert.Equal("2026/07_w03", ((XmlElement)levels[2]!).GetAttribute("name")); + } +} + +/// FdsMfr.UpdateNeed parsing + enum contract. +public class FdsMfrUpdateNeedTests +{ + [Theory] + [InlineData("Reset", FdsMfr.UpdateNeed.Reset)] + [InlineData("Full", FdsMfr.UpdateNeed.Full)] + [InlineData("Short", FdsMfr.UpdateNeed.Short)] + [InlineData("None", FdsMfr.UpdateNeed.None)] + public void UpdateNeedValue_ValidName_Parses(string name, FdsMfr.UpdateNeed expected) + { + Assert.Equal(expected, FdsMfr.UpdateNeedValue(name)); + } + + [Fact] + public void UpdateNeedValue_UnknownName_Throws() + { + Assert.ThrowsAny(() => FdsMfr.UpdateNeedValue("Bogus")); + } + + [Fact] + public void UpdateNeed_NumericValues_AreStable() + { + // These map to SQL updateneed codes — must not drift. + Assert.Equal(5, (int)FdsMfr.UpdateNeed.Reset); + Assert.Equal(2, (int)FdsMfr.UpdateNeed.Full); + Assert.Equal(1, (int)FdsMfr.UpdateNeed.Short); + Assert.Equal(0, (int)FdsMfr.UpdateNeed.None); + } +} + +/// FdsShared utility helpers — pure formatting + local file I/O. +public class FdsSharedTests +{ + [Theory] + [InlineData((byte)0)] + [InlineData((byte)1)] + [InlineData((byte)16)] + [InlineData((byte)32)] + public void RandomString_ReturnsRequestedLength_AlphanumericOnly(byte length) + { + var s = FdsShared.RandomString(length); + Assert.Equal(length, s.Length); + Assert.All(s, c => Assert.True(char.IsLetterOrDigit(c), $"unexpected char '{c}'")); + } + + [Fact] + public void ToCsv_DataRow_QuotesAndEscapesStringsWhenRequested() + { + var tbl = new DataTable(); + tbl.Columns.Add("Name", typeof(string)); + var row = tbl.NewRow(); + row["Name"] = "Say \"hi\""; + tbl.Rows.Add(row); + + Assert.Equal("\"Say \"\"hi\"\"\"", row.ToCsv(quoteStrings: true, CultureInfo.InvariantCulture)); + Assert.Equal("Say \"hi\"", row.ToCsv(quoteStrings: false, CultureInfo.InvariantCulture)); + } + + [Fact] + public void ToCsv_DataRow_NullAndDbNull_RenderAsEmpty() + { + var tbl = new DataTable(); + tbl.Columns.Add("A", typeof(string)); + tbl.Columns.Add("B", typeof(string)); + var row = tbl.NewRow(); + row["A"] = DBNull.Value; + row["B"] = "x"; + + Assert.Equal(";x", row.ToCsv(quoteStrings: false, CultureInfo.InvariantCulture, delimiter: ";")); + } + + [Fact] + public void ToCsv_DataRow_FormatsDecimalWithSuppliedCulture() + { + var tbl = new DataTable(); + tbl.Columns.Add("V", typeof(decimal)); + var row = tbl.NewRow(); + row["V"] = 1234.5m; + tbl.Rows.Add(row); + + Assert.Equal("1234.5", row.ToCsv(quoteStrings: false, CultureInfo.InvariantCulture)); + Assert.Equal("1234,5", row.ToCsv(quoteStrings: false, new CultureInfo("de-DE"))); + } + + [Fact] + public void ToCsv_DataTable_IncludesHeaderRow_WhenRequested() + { + var tbl = new DataTable(); + tbl.Columns.Add("Konto", typeof(string)); + tbl.Columns.Add("Betrag", typeof(decimal)); + tbl.Rows.Add("1200", 5m); + tbl.Rows.Add("1400", 6m); + + var csv = tbl.ToCsv(includeHeaders: true, quoteStrings: false, fieldDelimiter: ";"); + var lines = csv.Split("\r\n"); + + Assert.Equal("Konto;Betrag", lines[0]); + Assert.Equal("1200;5", lines[1]); + Assert.Equal("1400;6", lines[2]); + } + + [Fact] + public void ToCsv_DataTable_CanOmitHeaders() + { + var tbl = new DataTable(); + tbl.Columns.Add("X", typeof(string)); + tbl.Rows.Add("a"); + + var csv = tbl.ToCsv(includeHeaders: false, quoteStrings: false); + Assert.Equal("a", csv); + } + + [Theory] + [InlineData("archive.zip", "archive")] + [InlineData("archive.tar.gz", "archive.tar")] + [InlineData("noext", "noext")] + public void NameBase_StripsFinalExtension(string fileName, string expected) + { + var fi = new FileInfo(Path.Combine(Path.GetTempPath(), fileName)); + Assert.Equal(expected, fi.NameBase()); + } + + [Theory] + [InlineData("Hello, DATEV!")] + [InlineData("Ümläute & Straße")] + public void ToByteArray_Utf8_RoundTrips(string input) + { + // Default encoding is Encoding.UTF8, so the StreamWriter emits a leading BOM. + var bytes = input.ToByteArray(); + var decoded = Encoding.UTF8.GetString(bytes).TrimStart(''); + Assert.Equal(input, decoded); + } + + [Fact] + public void ToByteArray_Iso8859_1_RoundTrips() + { + const string input = "Grüße"; + var latin1 = Encoding.GetEncoding("ISO-8859-1"); + var bytes = input.ToByteArray(latin1); + Assert.Equal(input, latin1.GetString(bytes)); + } + + [Fact] + public void ReadWriteStream_CopiesFullContent() + { + var payload = Encoding.UTF8.GetBytes("stream-copy-payload-0123456789"); + using var src = new MemoryStream(payload); + using var dst = new MemoryStream(); + + Assert.True(FdsShared.ReadWriteStream(src, dst, closeWriteStream: false)); + Assert.Equal(payload, dst.ToArray()); + } + + [Fact] + public void WriteStreamToDisk_PersistsStreamToFile() + { + var payload = Encoding.UTF8.GetBytes("disk-payload"); + var path = Path.Combine(Path.GetTempPath(), $"fds_test_{Guid.NewGuid():N}.bin"); + try + { + using var src = new MemoryStream(payload); + Assert.True(FdsShared.WriteStreamToDisk(src, path)); + Assert.True(File.Exists(path)); + Assert.Equal(payload, File.ReadAllBytes(path)); + } + finally + { + if (File.Exists(path)) File.Delete(path); + } + } +} + +/// +/// FdsConfig + FdsMfrClient share process-global config state (FdsConfig._config), +/// so their tests are grouped into one collection to run sequentially and never +/// clobber each other's call. +/// +[CollectionDefinition("FdsConfig")] +public class FdsConfigCollection { } + +[Collection("FdsConfig")] +public class FdsConfigTests +{ + private static IConfiguration BuildConfig(Dictionary values) => + new ConfigurationBuilder().AddInMemoryCollection(values).Build(); + + [Fact] + public void FDSConnectionString_ReturnsConfiguredValue() + { + FdsConfig.Initialize(BuildConfig(new() + { + ["ConnectionStrings:fuchs_fds_ConnectionString"] = "Server=.;Database=fds;", + })); + Assert.Equal("Server=.;Database=fds;", FdsConfig.FDSConnectionString()); + } + + [Fact] + public void FDSConnectionString_Missing_Throws() + { + FdsConfig.Initialize(BuildConfig(new())); + Assert.Throws(() => FdsConfig.FDSConnectionString()); + } + + [Fact] + public void MfrSettings_ResolveFromFdsSection() + { + FdsConfig.Initialize(BuildConfig(new() + { + ["Fds:MFR_UserName"] = "system@example.com", + ["Fds:MFR_Password"] = "secret", + ["Fds:MFR_host"] = "portal.mobilefieldreport.com", + })); + + Assert.Equal("system@example.com", FdsConfig.MFR_UserName); + Assert.Equal("secret", FdsConfig.MFR_Password); + Assert.Equal("portal.mobilefieldreport.com", FdsConfig.MFR_host); + } + + [Fact] + public void MfrSettings_Absent_FallBackToEmptyString() + { + FdsConfig.Initialize(BuildConfig(new())); + Assert.Equal("", FdsConfig.MFR_UserName); + Assert.Equal("", FdsConfig.MFR_Password); + Assert.Equal("", FdsConfig.MFR_host); + } +} + +[Collection("FdsConfig")] +public class FdsMfrClientTests +{ + private static void InitConfig(string host) => + FdsConfig.Initialize(new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["Fds:MFR_host"] = host, + ["Fds:MFR_UserName"] = "u", + ["Fds:MFR_Password"] = "p", + }).Build()); + + [Fact] + public void Construction_BuildsClientConfig_FromHost() + { + InitConfig("portal.mobilefieldreport.com"); + using var client = new FdsMfrClient(); + Assert.Contains("portal.mobilefieldreport.com", client.ClientConfig.BaseUrl); + Assert.EndsWith("/", client.ClientConfig.BaseUrl); + } + + [Fact] + public void IsReadonly_DefaultsTrue_AndIsSettable() + { + InitConfig("portal.mobilefieldreport.com"); + using var client = new FdsMfrClient(); + Assert.True(client.IsReadonly); + client.IsReadonly = false; + Assert.False(client.IsReadonly); + } +} diff --git a/Fuchs.Tests/FdsInvoiceDataBlocksTests.cs b/Fuchs.Tests/FdsInvoiceDataBlocksTests.cs new file mode 100644 index 0000000..0af1bc9 --- /dev/null +++ b/Fuchs.Tests/FdsInvoiceDataBlocksTests.cs @@ -0,0 +1,52 @@ +using System.Linq; +using Fuchs.intranet; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Verifies the block projection that feeds the PDF item table: each service-request +/// group exposes its heading (the section title the editor shows) and its line items, +/// so the PDF can print a heading row per block and the flat item list still works. +/// +public class FdsInvoiceDataBlocksTests +{ + private static FdsInvoiceData FromReq(string reqJson) => + new(JObject.Parse(@"{'admin':{'type':'r'},'new':{},'sms':{},'req':" + reqJson + "}")); + + [Fact] + public void InvoiceBlocks_ExposesHeadingFromTextThenNme_AndItems() + { + var inv = FromReq(@"[ + {'Id':'1','text':'Sektion A','items':[{'id':'a','type':'material','title':'X','price_net':10,'total_net':10}]}, + {'Id':'2','nme':'Sektion B','items':[{'id':'b','type':'material','title':'Y','price_net':20,'total_net':20}]} + ]"); + + var blocks = inv.InvoiceBlocks; + + Assert.Equal(2, blocks.Count); + Assert.Equal("Sektion A", blocks[0].Heading); + Assert.Equal("Sektion B", blocks[1].Heading); // falls back to nme + Assert.Single(blocks[0].Items); + Assert.Equal("X", blocks[0].Items[0]["title"]); + } + + [Fact] + public void InvoiceBlocks_MissingHeading_IsEmpty() + { + var inv = FromReq(@"[{'Id':'1','items':[{'id':'a','type':'material','total_net':5}]}]"); + Assert.Equal("", Assert.Single(inv.InvoiceBlocks).Heading); + } + + [Fact] + public void InvoiceItems_StillFlattensAcrossBlocks() + { + var inv = FromReq(@"[ + {'Id':'1','text':'A','items':[{'id':'a','type':'material','total_net':10}]}, + {'Id':'2','text':'B','items':[{'id':'b','type':'material','total_net':20},{'id':'c','type':'material','total_net':30}]} + ]"); + + Assert.Equal(new[] { "a", "b", "c" }, inv.InvoiceItems.Select(i => i["id"]!.ToString()).ToArray()); + } +} diff --git a/Fuchs.Tests/Fuchs.Tests.csproj b/Fuchs.Tests/Fuchs.Tests.csproj index 1e28bd3..0c6f5cc 100644 --- a/Fuchs.Tests/Fuchs.Tests.csproj +++ b/Fuchs.Tests/Fuchs.Tests.csproj @@ -9,8 +9,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Fuchs.Tests/InvoiceDraftCacheTests.cs b/Fuchs.Tests/InvoiceDraftCacheTests.cs new file mode 100644 index 0000000..220ba80 --- /dev/null +++ b/Fuchs.Tests/InvoiceDraftCacheTests.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Fuchs.intranet; +using Fuchs.Notifications; +using Fuchs.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Covers the in-memory draft cache (storage + idle sliding TTL) and the background +/// expiry monitor that warns before eviction and closes the editor on eviction (ADR 0006). +/// +public class InvoiceDraftCacheTests +{ + private static IConfiguration Config(int idle = 30, int warn = 5) => + new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["Fuchs:DraftEditing:IdleMinutes"] = idle.ToString(), + ["Fuchs:DraftEditing:ExpiryWarnMinutes"] = warn.ToString() + }).Build(); + + private sealed class FakeNotifier : IDraftNotifier + { + public readonly List<(string token, int version)> Ready = new(); + public readonly List<(string token, int secondsLeft)> Expiring = new(); + public readonly List<(string token, string reason)> Closed = new(); + public Task SignalDraftReadyAsync(string token, int version, CancellationToken ct = default) { Ready.Add((token, version)); return Task.CompletedTask; } + public Task SignalExpiringAsync(string token, int secondsLeft, CancellationToken ct = default) { Expiring.Add((token, secondsLeft)); return Task.CompletedTask; } + public Task SignalClosedAsync(string token, string reason, CancellationToken ct = default) { Closed.Add((token, reason)); return Task.CompletedTask; } + } + + // ── Cache storage ───────────────────────────────────────────────────────── + [Fact] + public void SetGet_RoundTrips_AndUnknownTokenIsNull() + { + var cache = new InvoiceDraftCache(Config()); + var s = new InvoiceDraftSession { Token = "abc" }; + cache.Set(s); + Assert.Same(s, cache.Get("abc")); + Assert.Null(cache.Get("nope")); + } + + [Fact] + public void Remove_EvictsSession() + { + var cache = new InvoiceDraftCache(Config()); + cache.Set(new InvoiceDraftSession { Token = "x" }); + Assert.NotNull(cache.Remove("x")); + Assert.Null(cache.Get("x")); + Assert.Null(cache.Remove("x")); + } + + [Fact] + public void Get_ResetsExpiryWarningFlag_SoAFreshWarningIsDue() + { + var cache = new InvoiceDraftCache(Config()); + var s = new InvoiceDraftSession { Token = "x", ExpiryWarningSent = true }; + cache.Set(s); + cache.Get("x"); + Assert.False(s.ExpiryWarningSent); + } + + // ── Expiry monitor ──────────────────────────────────────────────────────── + [Fact] + public async Task Sweep_NearTtl_WarnsOnceThenEvictsWithReason() + { + var cfg = Config(idle: 30, warn: 5); + var cache = new InvoiceDraftCache(cfg); + var notifier = new FakeNotifier(); + var svc = new InvoiceDraftExpiryService(cache, notifier, cfg, NullLogger.Instance); + + var s = new InvoiceDraftSession { Token = "a" }; + cache.Set(s); + + // Idle 26 min → inside the 5-min warning window (30-5=25) but not yet expired. + s.LastAccessUtc = DateTime.UtcNow.AddMinutes(-26); + await svc.SweepAsync(CancellationToken.None); + Assert.Single(notifier.Expiring); + Assert.Empty(notifier.Closed); + Assert.True(s.ExpiryWarningSent); + + // Another sweep while still idle must not spam a second warning. + await svc.SweepAsync(CancellationToken.None); + Assert.Single(notifier.Expiring); + + // Past the TTL → evicted and the editor is told to close with a reason. + s.LastAccessUtc = DateTime.UtcNow.AddMinutes(-31); + await svc.SweepAsync(CancellationToken.None); + Assert.Single(notifier.Closed); + Assert.Equal(("a", "expired"), notifier.Closed[0]); + Assert.Null(cache.Get("a")); + } + + [Fact] + public async Task Sweep_FreshSession_DoesNothing() + { + var cfg = Config(idle: 30, warn: 5); + var cache = new InvoiceDraftCache(cfg); + var notifier = new FakeNotifier(); + var svc = new InvoiceDraftExpiryService(cache, notifier, cfg, NullLogger.Instance); + cache.Set(new InvoiceDraftSession { Token = "fresh" }); + + await svc.SweepAsync(CancellationToken.None); + + Assert.Empty(notifier.Expiring); + Assert.Empty(notifier.Closed); + } +} diff --git a/Fuchs.Tests/InvoiceDraftCalculatorTests.cs b/Fuchs.Tests/InvoiceDraftCalculatorTests.cs new file mode 100644 index 0000000..22352c6 --- /dev/null +++ b/Fuchs.Tests/InvoiceDraftCalculatorTests.cs @@ -0,0 +1,294 @@ +using Fuchs.intranet; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Verifies the server-side aggregation of invoice draft totals (the port of the +/// former client-side invSumUpdate footer math). The truth now lives in the +/// backend (ADR 0006), so this is unit-testable directly. Line values are read from +/// each block's itm array (the editor's co objects: vt=net, +/// vv=VAT, vs=service-net, vsv=service-VAT, vat=rate). +/// +public class InvoiceDraftCalculatorTests +{ + private static InvoiceDraftSession SessionWith(string reqJson, bool p13b = false) + { + var s = new InvoiceDraftSession { Token = "t" }; + s.Admin = new JObject { ["p13b"] = p13b }; + s.New = new JObject { ["invoiceemail"] = "kunde@example.de", ["invoiceaddress"] = "Weg 1" }; + s.Req = JArray.Parse(reqJson); + return s; + } + + // ── RecomputeLineValues ──────────────────────────────────────────────────── + private static JObject Line(InvoiceDraftSession s, int block, int line) => + (JObject)((JArray)((JObject)s.Req[block])["itm"]!)[line]; + + [Fact] + public void RecomputeLineValues_ComputesNetAndVatFromRawQtyPriceAndRate() + { + var s = SessionWith(@"[ + { 'Id':'10','itm':[ {'id':'a','typ':'material','qn':2,'v':50,'vat':'19%'} ] } + ]"); + + InvoiceDraftCalculator.RecomputeLineValues(s); + + var l = Line(s, 0, 0); + Assert.Equal(100m, l["vt"]!.Value()); + Assert.Equal(19m, l["vv"]!.Value()); + Assert.Equal(0m, l["vs"]!.Value()); + Assert.Equal(0m, l["vsv"]!.Value()); + } + + [Fact] + public void RecomputeLineValues_ServiceType_AlsoFillsServiceNetAndVat() + { + var s = SessionWith(@"[ + { 'Id':'10','itm':[ {'id':'a','typ':'Service','qn':3,'v':10,'vat':'19%'} ] } + ]"); + + InvoiceDraftCalculator.RecomputeLineValues(s); + + var l = Line(s, 0, 0); + Assert.Equal(30m, l["vt"]!.Value()); + Assert.Equal(5.7m, l["vv"]!.Value()); + Assert.Equal(30m, l["vs"]!.Value()); + Assert.Equal(5.7m, l["vsv"]!.Value()); + } + + [Theory] + [InlineData(0, 50)] // no quantity posted -> leave value untouched + [InlineData(2, 0)] // no price posted -> leave value untouched + public void RecomputeLineValues_MissingQtyOrPrice_LeavesExistingValueUntouched(decimal qty, decimal price) + { + var s = SessionWith($@"[ + {{ 'Id':'10','itm':[ {{'id':'a','typ':'material','qn':{qty},'v':{price},'vat':'19%','vt':777,'vv':111}} ] }} + ]"); + + InvoiceDraftCalculator.RecomputeLineValues(s); + + var l = Line(s, 0, 0); + Assert.Equal(777m, l["vt"]!.Value()); // untouched — mirrors quantChange's own guard + Assert.Equal(111m, l["vv"]!.Value()); + } + + [Fact] + public void RecomputeLineValues_ZeroVatRate_ComputesNetWithNoVat() + { + var s = SessionWith(@"[ + { 'Id':'10','itm':[ {'id':'a','typ':'material','qn':4,'v':25,'vat':'0%'} ] } + ]"); + + InvoiceDraftCalculator.RecomputeLineValues(s); + + var l = Line(s, 0, 0); + Assert.Equal(100m, l["vt"]!.Value()); + Assert.Equal(0m, l["vv"]!.Value()); + } + + [Fact] + public void RecomputeLineValues_SetHeaderConvertedSum_IsNotClobberedByRecompute() + { + // A converted set header carries a synthesised sum (no raw qty/price of its own) — + // RecomputeLineValues must never overwrite it, mirroring quantChange's guard. + var s = SessionWith(@"[ + { 'Id':'10','itm':[ {'id':'h','typ':'set','vt':1000,'vv':190,'vat':'19%'} ] } + ]"); + + InvoiceDraftCalculator.RecomputeLineValues(s); + + var l = Line(s, 0, 0); + Assert.Equal(1000m, l["vt"]!.Value()); + Assert.Equal(190m, l["vv"]!.Value()); + } + + [Fact] + public void RecomputeTotals_SumsNetVatServiceAndPerBlock() + { + var s = SessionWith(@"[ + { 'Id':'10','itm':[ + {'vt':100,'vv':19,'vs':0,'vsv':0,'vat':'19%'}, + {'vt':50,'vv':9.5,'vs':50,'vsv':9.5,'vat':'19%'} ] }, + { 'Id':'11','itm':[ + {'vt':200,'vv':14,'vs':0,'vsv':0,'vat':'7%'} ] } + ]"); + + InvoiceDraftCalculator.RecomputeTotals(s); + + Assert.Equal(350m, s.Sums.TotalNet); + Assert.Equal(42.5m, s.Sums.TotalVat); + Assert.Equal(392.5m, s.Sums.TotalGross); + Assert.Equal(50m, s.Sums.ServiceNet); + Assert.Equal(9.5m, s.Sums.ServiceVat); + Assert.Equal(28.5m, s.Sums.VatByRate["19"]); + Assert.Equal(14m, s.Sums.VatByRate["7"]); + Assert.Equal(150m, s.Sums.NetByBlock["10"]); + Assert.Equal(200m, s.Sums.NetByBlock["11"]); + } + + [Fact] + public void RecomputeTotals_ReverseCharge_SuppressesVatAndGrossEqualsNet() + { + var s = SessionWith(@"[{ 'Id':'1','itm':[ {'vt':100,'vv':19,'vat':'19%'} ] }]", p13b: true); + + InvoiceDraftCalculator.RecomputeTotals(s); + + Assert.Equal(100m, s.Sums.TotalNet); + Assert.Equal(100m, s.Sums.TotalGross); + Assert.Equal(0m, s.Sums.TotalVat); + Assert.Empty(s.Sums.VatByRate); + } + + [Fact] + public void RecomputeTotals_EmptyDraft_AllZero() + { + var s = SessionWith("[]"); + InvoiceDraftCalculator.RecomputeTotals(s); + Assert.Equal(0m, s.Sums.TotalNet); + Assert.Equal(0m, s.Sums.TotalGross); + Assert.Empty(s.Sums.VatByRate); + } + + [Theory] + [InlineData("19,0%", "19")] + [InlineData("7%", "7")] + [InlineData("19", "19")] + [InlineData("", "")] + [InlineData("0", "")] + [InlineData("7,5", "7.5")] + public void NormalizeRate_CanonicalisesRateStrings(string raw, string expected) + => Assert.Equal(expected, InvoiceDraftCalculator.NormalizeRate(raw)); + + [Fact] + public void Validate_ValidDraft_NoErrors() + { + var s = SessionWith(@"[{ 'Id':'1','itm':[ {'vt':100,'vv':19,'vat':'19%'} ] }]"); + InvoiceDraftCalculator.RecomputeTotals(s); + InvoiceDraftCalculator.Validate(s); + Assert.DoesNotContain(s.ValidationMessages, m => m.Severity == "error"); + } + + [Theory] + [InlineData("", "warning")] + [InlineData("not-an-email", "error")] + public void Validate_EmailProblems_AreFlagged(string email, string severity) + { + var s = SessionWith(@"[{ 'Id':'1','itm':[{'vt':10,'vat':'19%'}] }]"); + s.New["invoiceemail"] = email; + InvoiceDraftCalculator.RecomputeTotals(s); + InvoiceDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "email" && m.Severity == severity); + } + + [Fact] + public void Validate_NoItems_IsError() + { + var s = SessionWith("[]"); + InvoiceDraftCalculator.RecomputeTotals(s); + InvoiceDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "items" && m.Severity == "error"); + } + + [Fact] + public void Validate_UnknownVatRate_IsWarning() + { + var s = SessionWith(@"[{ 'Id':'1','itm':[{'vt':10,'vv':0.5,'vat':'5%'}] }]"); + InvoiceDraftCalculator.RecomputeTotals(s); + InvoiceDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "vat" && m.Severity == "warning"); + } + + [Fact] + public void Validate_MissingAddress_IsWarning() + { + var s = SessionWith(@"[{ 'Id':'1','itm':[{'vt':10,'vat':'19%'}] }]"); + s.New["invoiceaddress"] = ""; + InvoiceDraftCalculator.RecomputeTotals(s); + InvoiceDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "address" && m.Severity == "warning"); + } + + [Fact] + public void Validate_NegativeTotal_IsWarning() + { + var s = SessionWith(@"[{ 'Id':'1','itm':[{'vt':-50,'vv':0,'vat':''}] }]"); + InvoiceDraftCalculator.RecomputeTotals(s); + InvoiceDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "total" && m.Severity == "warning"); + } + + // ── RecomputePositions ──────────────────────────────────────────────────── + private static string Pos(InvoiceDraftSession s, int block, int line) => + ((JObject)((JArray)((JObject)s.Req[block])["itm"]!)[line])["p"]!.ToString(); + + [Fact] + public void RecomputePositions_NumbersPricedLinesContinuouslyAcrossBlocks() + { + var s = SessionWith(@"[ + { 'Id':'10','itm':[ {'id':'a','typ':'material','vt':1}, {'id':'b','typ':'service','vt':2} ] }, + { 'Id':'11','itm':[ {'id':'c','typ':'material','vt':3} ] } + ]"); + + InvoiceDraftCalculator.RecomputePositions(s); + + Assert.Equal("1", Pos(s, 0, 0)); + Assert.Equal("2", Pos(s, 0, 1)); + Assert.Equal("3", Pos(s, 1, 0)); // continuous, not restarting per block + } + + [Fact] + public void RecomputePositions_SkipsHeadingAndFreeTextLines() + { + var s = SessionWith(@"[ + { 'Id':'10','itm':[ + {'id':'t','typ':'Title','vt':0}, + {'id':'a','typ':'material','vt':1}, + {'id':'x','typ':'Text','vt':0}, + {'id':'b','typ':'material','vt':2} ] } + ]"); + + InvoiceDraftCalculator.RecomputePositions(s); + + Assert.Equal("", Pos(s, 0, 0)); // title carries no number + Assert.Equal("1", Pos(s, 0, 1)); + Assert.Equal("", Pos(s, 0, 2)); // free text carries no number + Assert.Equal("2", Pos(s, 0, 3)); + } + + [Fact] + public void RecomputePositions_NumbersSetHeaderLikeAnyItem() + { + // A set header is numbered just like the editor numbers it — only text/title lines are skipped. + var s = SessionWith(@"[ + { 'Id':'10','itm':[ + {'id':'h','typ':'set','vt':1000}, + {'id':'a','typ':'material','vt':600}, + {'id':'b','typ':'material','vt':400} ] } + ]"); + + InvoiceDraftCalculator.RecomputePositions(s); + + Assert.Equal("1", Pos(s, 0, 0)); // set header keeps position 1 (matches the editor) + Assert.Equal("2", Pos(s, 0, 1)); + Assert.Equal("3", Pos(s, 0, 2)); + } + + [Fact] + public void RecomputePositions_AfterBlockOrderChange_RenumbersToNewSequence() + { + var s = SessionWith(@"[ + { 'Id':'10','itm':[ {'id':'a','typ':'material','vt':1} ] }, + { 'Id':'11','itm':[ {'id':'b','typ':'material','vt':2} ] } + ]"); + // Simulate a section reorder: swap the two blocks. + var b0 = s.Req[0]; var b1 = s.Req[1]; + s.Req = new JArray(b1.DeepClone(), b0.DeepClone()); + + InvoiceDraftCalculator.RecomputePositions(s); + + Assert.Equal("1", Pos(s, 0, 0)); // formerly block 11's item is now position 1 + Assert.Equal("2", Pos(s, 1, 0)); + } +} diff --git a/Fuchs.Tests/InvoiceDraftNotifierTests.cs b/Fuchs.Tests/InvoiceDraftNotifierTests.cs new file mode 100644 index 0000000..3b1093b --- /dev/null +++ b/Fuchs.Tests/InvoiceDraftNotifierTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Fuchs.Notifications; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Logging.Abstractions; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Verifies targets the draft's SignalR group (keyed by +/// session token, ADR 0007) with the right method/payload, and — like the other +/// notification path — swallows hub failures so a missed coordination ping never +/// fails the underlying operation. +/// +public class InvoiceDraftNotifierTests +{ + private sealed class CapturingClientProxy : IClientProxy + { + private readonly bool _throw; + public string? Method { get; private set; } + public object?[]? Args { get; private set; } + public CapturingClientProxy(bool doThrow = false) => _throw = doThrow; + public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) + { + if (_throw) throw new InvalidOperationException("hub down"); + Method = method; + Args = args; + return Task.CompletedTask; + } + } + + private sealed class StubHubClients : IHubClients + { + private readonly IClientProxy _proxy; + public string? RequestedGroup { get; private set; } + public StubHubClients(IClientProxy proxy) => _proxy = proxy; + public IClientProxy Group(string groupName) { RequestedGroup = groupName; return _proxy; } + public IClientProxy All => throw new NotImplementedException(); + public IClientProxy AllExcept(IReadOnlyList e) => throw new NotImplementedException(); + public IClientProxy Client(string c) => throw new NotImplementedException(); + public IClientProxy Clients(IReadOnlyList c) => throw new NotImplementedException(); + public IClientProxy Groups(IReadOnlyList g) => throw new NotImplementedException(); + public IClientProxy GroupExcept(string g, IReadOnlyList e) => throw new NotImplementedException(); + public IClientProxy User(string u) => throw new NotImplementedException(); + public IClientProxy Users(IReadOnlyList u) => throw new NotImplementedException(); + } + + private sealed class StubHubContext : IHubContext + { + public StubHubContext(IHubClients clients) => Clients = clients; + public IHubClients Clients { get; } + public IGroupManager Groups => throw new NotImplementedException(); + } + + private static (DraftNotifier notifier, StubHubClients clients, CapturingClientProxy proxy) Create(bool doThrow = false) + { + var proxy = new CapturingClientProxy(doThrow); + var clients = new StubHubClients(proxy); + var notifier = new DraftNotifier(new StubHubContext(clients), NullLogger.Instance); + return (notifier, clients, proxy); + } + + [Fact] + public async Task SignalDraftReadyAsync_SendsToTokenGroupWithVersion() + { + var (notifier, clients, proxy) = Create(); + + await notifier.SignalDraftReadyAsync("tok-1", 7); + + Assert.Equal("tok-1", clients.RequestedGroup); + Assert.Equal("draftReady", proxy.Method); + var payload = JObject.FromObject(proxy.Args![0]!); + Assert.Equal("tok-1", payload["token"]!.Value()); + Assert.Equal(7, payload["version"]!.Value()); + } + + [Fact] + public async Task SignalExpiringAsync_SendsSecondsLeft() + { + var (notifier, _, proxy) = Create(); + + await notifier.SignalExpiringAsync("tok-2", 120); + + Assert.Equal("draftExpiring", proxy.Method); + var payload = JObject.FromObject(proxy.Args![0]!); + Assert.Equal(120, payload["secondsLeft"]!.Value()); + } + + [Fact] + public async Task SignalClosedAsync_SendsReason() + { + var (notifier, _, proxy) = Create(); + + await notifier.SignalClosedAsync("tok-3", "expired"); + + Assert.Equal("draftClosed", proxy.Method); + var payload = JObject.FromObject(proxy.Args![0]!); + Assert.Equal("expired", payload["reason"]!.Value()); + } + + [Fact] + public async Task EmptyToken_DoesNotSend() + { + var (notifier, clients, proxy) = Create(); + + await notifier.SignalDraftReadyAsync("", 1); + + Assert.Null(clients.RequestedGroup); + Assert.Null(proxy.Method); + } + + [Fact] + public async Task HubFailure_IsSwallowed() + { + var (notifier, _, _) = Create(doThrow: true); + + // Must not throw — a failed coordination ping cannot fail the caller's operation. + await notifier.SignalDraftReadyAsync("tok", 1); + await notifier.SignalExpiringAsync("tok", 60); + await notifier.SignalClosedAsync("tok", "expired"); + } +} diff --git a/Fuchs.Tests/InvoiceDraftServiceTests.cs b/Fuchs.Tests/InvoiceDraftServiceTests.cs new file mode 100644 index 0000000..222836d --- /dev/null +++ b/Fuchs.Tests/InvoiceDraftServiceTests.cs @@ -0,0 +1,842 @@ +using System.Linq; +using System.Threading.Tasks; +using Fuchs.intranet; +using Fuchs.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using MigraDoc.DocumentObjectModel; +using Newtonsoft.Json.Linq; +using OCORE.security; +using Xunit; +using static OCORE.OCORE_dictionaries; + +namespace Fuchs.Tests; + +/// +/// Exercises the draft edit orchestrator's pure paths (open/patch/history/flush) +/// without a database, proving the backend-authoritative model behaves correctly at +/// the service seam (ADR 0006). Blocks use the editor's itm/items shape. +/// +public class InvoiceDraftServiceTests +{ + /// Captures the invoice handed to registration and returns it with a fake DB id — no SQL. + private sealed class FakeInvoiceService : IInvoiceService + { + public FdsInvoiceData? Registered; + public bool? LastChange; + public Task RegisterInvoiceAsync(FdsInvoiceData invoice, bool change, string invId, string userAccountId, DatabaseSecurity dbSec) + { + Registered = invoice; + LastChange = change; + invoice.InvoiceRegistration = new GenericObjectDictionary(new System.Collections.Generic.Dictionary { ["Id"] = "INV42" }); + return Task.FromResult(invoice); + } + public FdsInvoiceData? PreviewInvoice; + public bool? PreviewDraft; + public Task LoadInvoiceAsync(string id, string u, DatabaseSecurity s) => throw new System.NotSupportedException(); + public Document GenerateInvoicePdf(FdsInvoiceData i, bool d) { PreviewInvoice = i; PreviewDraft = d; return new Document(); } + public Task RenderInvoicePdfBytesAsync(FdsInvoiceData i, bool d) => throw new System.NotSupportedException(); + public Task StoreInvoiceDocumentFileAsync(FdsInvoiceData i, bool d, string u, DatabaseSecurity s) => throw new System.NotSupportedException(); + public Task GetInvoiceFileAsync(FdsInvoiceData i, bool d, fds.IFdsMfr m) => throw new System.NotSupportedException(); + } + + private static (InvoiceDraftEditService svc, FakeInvoiceService inv, InvoiceDraftCache cache) NewService() + { + var cache = new InvoiceDraftCache(new ConfigurationBuilder().Build()); + var inv = new FakeInvoiceService(); + var svc = new InvoiceDraftEditService(cache, inv, NullLogger.Instance); + return (svc, inv, cache); + } + + private static JObject Payload() => JObject.Parse(@"{ + 'admin':{'p13b':false,'type':'r','paymentterms':'10wd'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'}, + 'req':[{'Id':'1','text':'Auftrag','itm':[{'id':'900','typ':'material','vt':100,'vv':19,'vs':0,'vsv':0,'vat':'19%'}], + 'items':[{'id':'900','type':'material','total_net':100,'vat':'19%'}]}] + }"); + + [Fact] + public void OpenFromPayload_SeedsSessionAndComputesTotals() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + Assert.False(string.IsNullOrEmpty(s.Token)); + Assert.Equal(0, s.Version); + Assert.Equal(100m, s.Sums.TotalNet); + Assert.Equal(119m, s.Sums.TotalGross); + } + + [Fact] + public void ApplyPatch_Email_MutatesBumpsVersionAndRecordsHistory() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "email", Value = JToken.FromObject("neu@x.de") }); + + Assert.NotNull(s2); + Assert.Equal(1, s2!.Version); + Assert.Equal("neu@x.de", s2.New["invoiceemail"]!.Value()); + var h = Assert.Single(s2.History); + Assert.Equal("email", h.Target); + Assert.Equal("a@b.de", h.OldValue); + Assert.Equal("neu@x.de", h.NewValue); + Assert.Equal(1, h.Version); + } + + [Fact] + public void ApplyPatch_BlockReplace_RecomputesTotals() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var newBlock = JObject.Parse(@"{'Id':'1','text':'Auftrag','itm':[{'id':'900','typ':'material','vt':50,'vv':9.5,'vat':'19%'}], + 'items':[{'id':'900','type':'material','total_net':50,'vat':'19%'}]}"); + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.replace", Ref = "1", Value = newBlock }); + + Assert.Equal(50m, s2!.Sums.TotalNet); + Assert.Equal(9.5m, s2.Sums.VatByRate["19"]); + } + + [Fact] + public void ApplyPatch_BlockRemove_EmptiesDraftAndFlagsNoItems() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.remove", Ref = "1" }); + + Assert.Equal(0m, s2!.Sums.TotalNet); + Assert.Contains(s2.ValidationMessages, m => m.Field == "items" && m.Severity == "error"); + } + + [Fact] + public void ApplyPatch_P13bToggle_FlipsAndSuppressesVat() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "p13b" }); // no value → toggle + + Assert.Equal(100m, s2!.Sums.TotalGross); // reverse-charge → gross == net + Assert.Empty(s2.Sums.VatByRate); + } + + [Fact] + public void ApplyPatch_UnknownToken_ReturnsNull() + { + var (svc, _, _) = NewService(); + Assert.Null(svc.ApplyPatch("ghost", new InvoiceDraftDelta { Target = "email", Value = JToken.FromObject("x@y.de") })); + } + + [Fact] + public async Task FlushToDbAsync_RegistersWithMappedTotals_AndSetsInvId() + { + var (svc, inv, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var result = await svc.FlushToDbAsync(s.Token, "user1", null!); + + Assert.NotNull(result); + Assert.Equal("INV42", result!.Id); + Assert.False(inv.LastChange); // new draft (no prior InvId) → create, not update + Assert.Equal("INV42", svc.Get(s.Token)!.InvId); + + var prms = inv.Registered!.BuildInvoiceParams(change: false, invId: ""); + var balance = prms.First(p => p.ParameterName == "@InvoiceBalance"); + Assert.Equal("119", System.Convert.ToString(balance.Value, System.Globalization.CultureInfo.InvariantCulture)); + var vatRate = prms.First(p => p.ParameterName == "@InvoiceVAT_1"); + Assert.Equal("19", vatRate.Value); + } + + [Fact] + public void GetHistory_UnknownToken_IsEmpty() + { + var (svc, _, _) = NewService(); + Assert.Empty(svc.GetHistory("ghost")); + } + + [Theory] + [InlineData("address", "invoiceaddress")] + [InlineData("title", "invoicetitle")] + [InlineData("provisionperiod", "provisionperiod")] + public void ApplyPatch_ScalarFieldDeltas_UpdateNew(string target, string newKey) + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = target, Value = JToken.FromObject("X-VALUE") }); + + Assert.Equal("X-VALUE", s2!.New[newKey]!.Value()); + } + + [Fact] + public void ApplyPatch_ProvisionLocation_MirrorsLocAndProvisionlocation() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "provisionlocation", Value = JToken.FromObject("Baustelle 7") }); + + Assert.Equal("Baustelle 7", s2!.New["provisionlocation"]!.Value()); + Assert.Equal("Baustelle 7", s2.New["loc"]!.Value()); + } + + [Fact] + public void ApplyPatch_Contact_BuildsCustomValuesJson() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta + { + Target = "contact", + Value = JObject.Parse(@"{'name':'Max Mustermann','email':'max@kunde.de'}") + }); + + var cv = JObject.Parse(s2!.New["CustomValues"]!.Value()!); + Assert.Equal("Max Mustermann", cv["contactName"]!.Value()); + Assert.Equal("max@kunde.de", cv["contactEmail"]!.Value()); + } + + [Fact] + public void ApplyPatch_SetmodeDelta_UpdatesAdmin() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "setmode", Value = JToken.FromObject("setonly") }); + + Assert.Equal("setonly", s2!.Admin["setmode"]!.Value()); + } + + [Fact] + public void ApplyPatch_P13bExplicitFalse_TurnsOffAndRestoresVat() + { + var (svc, _, _) = NewService(); + var payload = Payload(); + payload["admin"]!["p13b"] = true; // start reverse-charge + var s = svc.OpenFromPayload(payload, "user1"); + Assert.Empty(s.Sums.VatByRate); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "p13b", Value = JToken.FromObject(false) }); + + Assert.Equal(119m, s2!.Sums.TotalGross); // VAT restored + Assert.Equal(19m, s2.Sums.VatByRate["19"]); + } + + [Fact] + public void ApplyPatch_BlockReplace_InsertsWhenBlockIsNew() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var newBlock = JObject.Parse(@"{'Id':'2','text':'Zusatz','itm':[{'id':'950','typ':'material','vt':30,'vv':5.7,'vat':'19%'}], + 'items':[{'id':'950','type':'material','total_net':30,'vat':'19%'}]}"); + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.replace", Ref = "2", Value = newBlock }); + + Assert.Equal(2, s2!.Req.Count); + Assert.Equal(130m, s2.Sums.TotalNet); // 100 (block 1) + 30 (new block 2) + Assert.Equal(30m, s2.Sums.NetByBlock["2"]); + } + + // fds__prepInvoice's [SetItmID] window function anchors on the still-unconverted (price 0) + // Set header: the header row's own SetItmId self-references its own id ('1'), never null — + // ApplyItemSetPrice must still recognize id-equality first and never treat the header as its + // own member (see ApplyItemSetPrice's id == Ref check). + private static JObject SetPayload() => JObject.Parse(@"{ + 'admin':{'p13b':false,'type':'r','paymentterms':'10wd'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'}, + 'req':[{'Id':'1','text':'Auftrag','itm':[ + {'id':'1','typ':'set','vt':0,'vv':0,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'}, + {'id':'2','typ':'material','vt':60,'vv':11.4,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'}, + {'id':'3','typ':'material','vt':40,'vv':7.6,'vs':10,'vsv':1.9,'vat':'19%','SetItmId':'1'}], + 'items':[{'id':'1','type':'set'},{'id':'2','type':'material','setId':'1','total_net':60,'vat':'19%'}, + {'id':'3','type':'material','setId':'1','total_net':40,'vat':'19%'}]}] + }"); + + // A fourth, unrelated item ('4') follows the set's members in the same block but was never + // attributed a SetItmId by fds__prepInvoice (it isn't part of the set) — it must stay untouched + // by the conversion, proving membership is driven purely by SetItmId, not row order/position. + private static JObject SetPayloadWithTrailingUnrelatedItem() => JObject.Parse(@"{ + 'admin':{'p13b':false,'type':'r','paymentterms':'10wd'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'}, + 'req':[{'Id':'1','text':'Auftrag','itm':[ + {'id':'1','typ':'set','vt':0,'vv':0,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'}, + {'id':'2','typ':'material','vt':60,'vv':11.4,'vs':0,'vsv':0,'vat':'19%','SetItmId':'1'}, + {'id':'3','typ':'material','vt':40,'vv':7.6,'vs':10,'vsv':1.9,'vat':'19%','SetItmId':'1'}, + {'id':'4','typ':'material','vt':25,'vv':4.75,'vs':0,'vsv':0,'vat':'19%','SetItmId':null}], + 'items':[{'id':'1','type':'set'},{'id':'2','type':'material','setId':'1','total_net':60,'vat':'19%'}, + {'id':'3','type':'material','setId':'1','total_net':40,'vat':'19%'}, + {'id':'4','type':'material','total_net':25,'vat':'19%'}]}] + }"); + + [Fact] + public void ApplyPatch_ItemSetPrice_SumsMembersOntoHeaderAndNullsMembers() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(SetPayload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" }); + + Assert.NotNull(s2); + Assert.Equal(1, s2!.Version); + var block = (JObject)s2.Req[0]; + var lines = (JArray)block["itm"]!; + var header = lines.OfType().Single(l => (string)l["id"]! == "1"); + var m2 = lines.OfType().Single(l => (string)l["id"]! == "2"); + var m3 = lines.OfType().Single(l => (string)l["id"]! == "3"); + + Assert.Equal(100m, header["vt"]!.Value()); // 60 + 40 + Assert.Equal(19m, header["vv"]!.Value()); // 11.4 + 7.6 + Assert.Equal(10m, header["vs"]!.Value()); + Assert.Equal(1.9m, header["vsv"]!.Value()); + Assert.Equal(JTokenType.Null, m2["vt"]!.Type); // ADR 0009: members nulled (empty cell), not 0 + Assert.Equal(JTokenType.Null, m2["vv"]!.Type); + Assert.Equal(JTokenType.Null, m3["vt"]!.Type); + Assert.Equal(JTokenType.Null, m3["vv"]!.Type); + + // Total invoice sum is unchanged by the conversion (set price == sum of members). + Assert.Equal(100m, s2.Sums.TotalNet); + Assert.Equal(19m, s2.Sums.TotalVat); + + var h = Assert.Single(s2.History); + Assert.Equal("item.setprice", h.Target); + Assert.Equal("1", h.Ref); + Assert.Equal("0", h.OldValue); + Assert.Equal("100", h.NewValue); + } + + [Fact] + public void ApplyPatch_ItemSetPrice_UnknownRef_IsNoOp() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(SetPayload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "999" }); + + Assert.NotNull(s2); + Assert.Equal(0, s2!.Version); + Assert.Empty(s2.History); + } + + [Fact] + public void ApplyPatch_ItemSetPrice_RefNotASetHeader_IsNoOp() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(SetPayload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "2" }); + + Assert.NotNull(s2); + Assert.Equal(0, s2!.Version); + Assert.Empty(s2.History); + } + + [Fact] + public void ApplyPatch_ItemSetPrice_HeaderSelfReferencingSetItmId_NeverCountsHeaderAsOwnMember() + { + // Regression test: fds__prepInvoice no longer nulls out the header's own SetItmId (it + // self-references its own id). ApplyItemSetPrice must still sum exactly the two real + // members (100 net), not 3x by also including the header as if it were a member of itself. + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(SetPayload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" }); + + Assert.NotNull(s2); + var block = (JObject)s2!.Req[0]; + var lines = (JArray)block["itm"]!; + var header = lines.OfType().Single(l => (string)l["id"]! == "1"); + + Assert.Equal(100m, header["vt"]!.Value()); // 60 + 40, not tripled by self-inclusion + Assert.Equal(100m, s2.Sums.TotalNet); + } + + [Fact] + public void ApplyPatch_ItemSetPrice_UnrelatedItemAfterMembers_IsNeverSweptIntoSet() + { + // Regression test for the reported bug: an item after the set's real members in the same + // block, but with no SetItmId of its own, must stay fully priced and untouched — only + // items the server actually tagged with SetItmId == the header id are members. + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(SetPayloadWithTrailingUnrelatedItem(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" }); + + Assert.NotNull(s2); + var block = (JObject)s2!.Req[0]; + var lines = (JArray)block["itm"]!; + var header = lines.OfType().Single(l => (string)l["id"]! == "1"); + var m2 = lines.OfType().Single(l => (string)l["id"]! == "2"); + var m3 = lines.OfType().Single(l => (string)l["id"]! == "3"); + var other = lines.OfType().Single(l => (string)l["id"]! == "4"); + + Assert.Equal(100m, header["vt"]!.Value()); // only the real members (60 + 40) + Assert.Equal(JTokenType.Null, m2["vt"]!.Type); // ADR 0009: nulled, not 0 + Assert.Equal(JTokenType.Null, m3["vt"]!.Type); + Assert.Equal(25m, other["vt"]!.Value()); // untouched — never part of the set + Assert.Equal(4.75m, other["vv"]!.Value()); + + // Total invoice sum unaffected: 100 (set) + 25 (unrelated item) = 125. + Assert.Equal(125m, s2.Sums.TotalNet); + } + + // ── Block set-price menu modes (ADR 0009): per-service-request-block, irreversible ── + private static JObject BlocksPayload() => JObject.Parse(@"{ + 'admin':{'p13b':false,'type':'r'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'}, + 'req':[ + {'Id':'1','text':'Auftrag A','itm':[ + {'id':'11','typ':'material','vt':60,'vv':11.4,'vs':0,'vsv':0,'vat':'19%'}, + {'id':'12','typ':'service','vt':40,'vv':7.6,'vs':40,'vsv':7.6,'vat':'19%'}], + 'items':[{'id':'11','type':'material','total_net':60,'vat':'19%'}, + {'id':'12','type':'service','total_net':40,'vat':'19%'}]}, + {'Id':'2','text':'Auftrag B','itm':[ + {'id':'21','typ':'material','vt':30,'vv':5.7,'vs':0,'vsv':0,'vat':'19%'}], + 'items':[{'id':'21','type':'material','total_net':30,'vat':'19%'}]}] + }"); + + [Fact] + public void ApplyPatch_BlockSetPrice_InsertsSetRowPerBlock_NullsMembers_TotalUnchanged() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(BlocksPayload(), "user1"); + Assert.Equal(130m, s.Sums.TotalNet); // 100 (A) + 30 (B) + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setprice" }); + + Assert.NotNull(s2); + Assert.Equal(1, s2!.Version); + + var linesA = (JArray)((JObject)s2.Req[0])["itm"]!; + Assert.Equal(3, linesA.Count); // set row + the two (nulled) members, kept + var setA = (JObject)linesA[0]; + Assert.Equal("set", (string)setA["typ"]!); + Assert.Equal("bset_1", (string)setA["id"]!); + Assert.Equal(100m, setA["vt"]!.Value()); // block sum + Assert.Equal(19m, setA["vv"]!.Value()); + Assert.Equal(40m, setA["vs"]!.Value()); // service-net split preserved + Assert.Equal(7.6m, setA["vsv"]!.Value()); + var m11 = linesA.OfType().Single(l => (string)l["id"]! == "11"); + Assert.Equal(JTokenType.Null, m11["vt"]!.Type); // ADR 0009: null (empty cell), not 0 + Assert.Equal(JTokenType.Null, m11["vv"]!.Type); + + // items contract mirrors it (PDF reads total_net from here). + var itemsA = (JArray)((JObject)s2.Req[0])["items"]!; + Assert.Equal("set", (string)((JObject)itemsA[0])["type"]!); + Assert.Equal(100m, ((JObject)itemsA[0])["total_net"]!.Value()); + Assert.Equal(JTokenType.Null, itemsA.OfType().Single(i => (string)i["id"]! == "11")["total_net"]!.Type); + + // second block converted too; total conserved. + Assert.Equal(30m, ((JObject)((JArray)((JObject)s2.Req[1])["itm"]!)[0])["vt"]!.Value()); + Assert.Equal(130m, s2.Sums.TotalNet); + Assert.Equal(24.7m, s2.Sums.TotalVat); + } + + [Fact] + public void ApplyPatch_BlockSetOnly_InsertsSetRowPerBlock_RemovesMembers_TotalUnchanged() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(BlocksPayload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setonly" }); + + Assert.NotNull(s2); + var linesA = (JArray)((JObject)s2!.Req[0])["itm"]!; + Assert.Single(linesA); // members removed, only the set row remains + Assert.Equal("set", (string)((JObject)linesA[0])["typ"]!); + Assert.Equal(100m, ((JObject)linesA[0])["vt"]!.Value()); + Assert.Single((JArray)((JObject)s2.Req[0])["items"]!); + Assert.Equal(130m, s2.Sums.TotalNet); // total unchanged + } + + [Fact] + public void ApplyPatch_BlockSetPrice_SetRowNotSweptIntoBuildSetDisplay() + { + // The block set row has no setId members -> HasSetMembers is false -> it renders flat + // (emphasised on its own, members null to blank), so BuildSetDisplay must not emit flags for it. + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(BlocksPayload(), "user1"); + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setprice" }); + + var state = JObject.FromObject(svc.BuildState(svc.Get(s.Token)!)); + var setDisplay = (JObject)state["setDisplay"]!; + Assert.False(setDisplay.ContainsKey("bset_1")); + Assert.Empty(setDisplay); + } + + [Fact] + public void ApplyPatch_BlockSetPrice_EmptyOrUnpricedBlocks_IsNoOp() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(JObject.Parse(@"{ + 'admin':{'p13b':false,'type':'r'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','invoicetitle':'Rechnung'}, + 'req':[{'Id':'1','text':'Leer','itm':[],'items':[]}] + }"), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.setprice" }); + + Assert.NotNull(s2); + Assert.Equal(0, s2!.Version); // nothing priced -> no-op + Assert.Empty(s2.History); + } + + [Fact] + public void ApplyPatch_UnknownTarget_IsNoOp_NoVersionBumpNoHistory() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "nonsense", Value = JToken.FromObject("x") }); + + Assert.NotNull(s2); + Assert.Equal(0, s2!.Version); + Assert.Empty(s2.History); + } + + [Fact] + public void ApplyPatch_MultipleEdits_AccumulateHistoryInOrder() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "email", Value = JToken.FromObject("a1@x.de") }); + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "title", Value = JToken.FromObject("Titel 2") }); + var s3 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "address", Value = JToken.FromObject("Adr 3") }); + + Assert.Equal(3, s3!.Version); + Assert.Equal(3, s3.History.Count); + Assert.Equal(new[] { "email", "title", "address" }, s3.History.Select(h => h.Target).ToArray()); + Assert.Equal(new[] { 1, 2, 3 }, s3.History.Select(h => h.Version).ToArray()); + } + + [Fact] + public void BuildState_ExposesPayloadSumsValidationAndVersion() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "email", Value = JToken.FromObject("x@y.de") }); + + var state = JObject.FromObject(svc.BuildState(svc.Get(s.Token)!)); + + Assert.Equal(1, state["version"]!.Value()); + Assert.Equal(100m, state["sums"]!["total_net"]!.Value()); + Assert.Equal(119m, state["sums"]!["total_gross"]!.Value()); + Assert.Equal(19m, state["sums"]!["vat"]!["19"]!.Value()); + Assert.Single((JArray)state["req"]!); + Assert.Equal(1, state["historyCount"]!.Value()); + Assert.NotNull(state["validation"]); + } + + [Fact] + public async Task FlushToDbAsync_ExistingInvId_UpdatesInsteadOfCreates() + { + var (svc, inv, _) = NewService(); + var payload = Payload(); + payload["invid"] = "INV7"; + var s = svc.OpenFromPayload(payload, "user1"); + + await svc.FlushToDbAsync(s.Token, "user1", null!); + + Assert.True(inv.LastChange); // prior InvId → update path + } + + [Fact] + public async Task FlushToDbAsync_MapsInvoiceOptionsFrom13bAndSetmode() + { + var (svc, inv, _) = NewService(); + var payload = Payload(); + payload["admin"]!["p13b"] = true; + payload["admin"]!["setmode"] = "setonly"; + var s = svc.OpenFromPayload(payload, "user1"); + + await svc.FlushToDbAsync(s.Token, "user1", null!); + + var options = inv.Registered!.BuildInvoiceParams(change: false, invId: "") + .First(p => p.ParameterName == "@InvoiceOptions").Value?.ToString() ?? ""; + Assert.Contains("§13b", options); + Assert.Contains("setmode:setonly", options); + } + + [Fact] + public void RenderPreview_SynthesizesDraftRegistrationFromSession() + { + var (svc, inv, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var doc = svc.RenderPreview(s.Token); + + Assert.NotNull(doc); + Assert.True(inv.PreviewDraft); // always rendered as a draft + var reg = inv.PreviewInvoice!.InvoiceRegistration!; + Assert.Equal("Rechnung", reg.getString("InvoiceTitle")); + Assert.Equal("Weg 1", reg.getString("SendToAddress")); + Assert.Equal("a@b.de", reg.getString("SendToEmail")); + Assert.Equal("19", reg.getString("InvoiceVAT_1")); // rate synthesised from server sums + Assert.True(inv.PreviewInvoice.IsDraft); + } + + [Fact] + public void RenderPreview_UnknownToken_ReturnsNull() + { + var (svc, _, _) = NewService(); + Assert.Null(svc.RenderPreview("ghost")); + } + + [Fact] + public void Close_RemovesSession_ThenReportsFalse() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + Assert.True(svc.Close(s.Token)); + Assert.Null(svc.Get(s.Token)); + Assert.False(svc.Close(s.Token)); + } + + // ── HTML sanitisation (values must never reach the DB/PDF wrapped in tags) ─ + [Theory] + [InlineData("provisionperiod", "provisionperiod")] + [InlineData("title", "invoicetitle")] + [InlineData("email", "invoiceemail")] + public void ApplyPatch_ScalarField_StripsHtmlWrapper(string target, string newKey) + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = target, Value = JToken.FromObject("

18.06.2026

") }); + + Assert.Equal("18.06.2026", s2!.New[newKey]!.Value()); // no

tags stored + Assert.Equal("18.06.2026", Assert.Single(s2.History).NewValue); + } + + [Fact] + public void ApplyPatch_Address_MultilineHtml_KeepsLineBreaks() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta + { + Target = "address", + Value = JToken.FromObject("

Firma AG

Weg 1
5080 Laufenburg

") + }); + + Assert.Equal("Firma AG\nWeg 1\n5080 Laufenburg", s2!.New["invoiceaddress"]!.Value()); + } + + [Fact] + public void ApplyPatch_ScalarField_DecodesEntities() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "title", Value = JToken.FromObject("Tom & Jerry") }); + + Assert.Equal("Tom & Jerry", s2!.New["invoicetitle"]!.Value()); + } + + [Fact] + public void ApplyPatch_ProvisionLocation_SanitisesAndMirrorsLoc() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "provisionlocation", Value = JToken.FromObject("

Baustelle 7

") }); + + Assert.Equal("Baustelle 7", s2!.New["provisionlocation"]!.Value()); + Assert.Equal("Baustelle 7", s2.New["loc"]!.Value()); + } + + // ── Change history records the changed field, not the whole block JSON ───── + [Fact] + public void ApplyPatch_BlockReplace_HistoryNewValueIsSectionText_NotJson() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var newBlock = JObject.Parse(@"{'Id':'1','text':'

Neue Überschrift

', + 'itm':[{'id':'900','typ':'material','vt':100,'vv':19,'vat':'19%'}], + 'items':[{'id':'900','type':'material','total_net':100,'vat':'19%'}]}"); + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.replace", Ref = "1", Value = newBlock }); + + var h = Assert.Single(s2!.History); + Assert.Equal("Neue Überschrift", h.NewValue); // the heading, sanitised — never the block JSON + Assert.DoesNotContain("{", h.NewValue); + Assert.Equal("Auftrag", h.OldValue); + // and the cached block text is stored clean too + Assert.Equal("Neue Überschrift", ((JObject)s2.Req[0])["text"]!.Value()); + } + + // ── Section reorder ─────────────────────────────────────────────────────── + private static JObject TwoBlockPayload() => JObject.Parse(@"{ + 'admin':{'p13b':false,'type':'r'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1'}, + 'req':[ + {'Id':'1','text':'A','itm':[{'id':'900','typ':'material','vt':100,'vv':19,'vat':'19%'}],'items':[{'id':'900','type':'material','total_net':100,'vat':'19%'}]}, + {'Id':'2','text':'B','itm':[{'id':'950','typ':'material','vt':30,'vv':5.7,'vat':'19%'}],'items':[{'id':'950','type':'material','total_net':30,'vat':'19%'}]} + ]}"); + + [Fact] + public void ApplyPatch_BlockOrder_ReordersReqAndRenumbersPositions() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(TwoBlockPayload(), "user1"); + Assert.Equal(new[] { "1", "2" }, s.Req.Select(b => b["Id"]!.Value()).ToArray()); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.order", Value = JArray.Parse("['2','1']") }); + + Assert.Equal(new[] { "2", "1" }, s2!.Req.Select(b => b["Id"]!.Value()).ToArray()); + Assert.Equal("1", ((JObject)((JArray)((JObject)s2.Req[0])["itm"]!)[0])["p"]!.ToString()); // block 2's item now position 1 + Assert.Equal(130m, s2.Sums.TotalNet); // totals unaffected by reorder + var h = Assert.Single(s2.History); + Assert.Equal("1,2", h.OldValue); + Assert.Equal("2,1", h.NewValue); + } + + [Fact] + public void ApplyPatch_BlockOrder_UnchangedSequence_IsNoOp() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(TwoBlockPayload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.order", Value = JArray.Parse("['1','2']") }); + + Assert.Equal(0, s2!.Version); // no-op: no version bump, no history + Assert.Empty(s2.History); + } + + [Fact] + public void ApplyPatch_BlockOrder_UnknownIds_KeepMentionedFirstThenRest() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(TwoBlockPayload(), "user1"); + + // Only name block 2; block 1 is unmentioned and must be kept (appended after). + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.order", Value = JArray.Parse("['2','ghost']") }); + + Assert.Equal(new[] { "2", "1" }, s2!.Req.Select(b => b["Id"]!.Value()).ToArray()); + } + + // ── Multiple VAT rates aggregate independently (ADR 0008: server owns every tax sum) ── + private static JObject MultiRatePayload() => JObject.Parse(@"{ + 'admin':{'p13b':false,'type':'r'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1'}, + 'req':[ + {'Id':'1','text':'A','itm':[ + {'id':'900','typ':'material','vt':100,'vv':19,'vat':'19%'}, + {'id':'901','typ':'material','vt':200,'vv':14,'vat':'7%'}, + {'id':'902','typ':'material','vt':50,'vv':0,'vat':'0%'}], + 'items':[{'id':'900','type':'material','total_net':100,'vat':'19%'}, + {'id':'901','type':'material','total_net':200,'vat':'7%'}, + {'id':'902','type':'material','total_net':50,'vat':'0%'}]} + ]}"); + + [Fact] + public void OpenFromPayload_MultipleVatRates_GroupsSumsPerRate() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(MultiRatePayload(), "user1"); + + Assert.Equal(350m, s.Sums.TotalNet); + Assert.Equal(33m, s.Sums.TotalVat); + Assert.Equal(383m, s.Sums.TotalGross); + Assert.Equal(19m, s.Sums.VatByRate["19"]); + Assert.Equal(14m, s.Sums.VatByRate["7"]); + Assert.False(s.Sums.VatByRate.ContainsKey("0")); // zero-rate contributes no VAT key (mirrors calculator) + } + + [Fact] + public void ApplyPatch_BlockReplace_MultiRate_RecomputesEachRateIndependently() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(MultiRatePayload(), "user1"); + + // Halve the 7%-rate line's net/VAT via a block replace; the 19% bucket must stay untouched. + var newBlock = JObject.Parse(@"{'Id':'1','text':'A','itm':[ + {'id':'900','typ':'material','vt':100,'vv':19,'vat':'19%'}, + {'id':'901','typ':'material','vt':100,'vv':7,'vat':'7%'}, + {'id':'902','typ':'material','vt':50,'vv':0,'vat':'0%'}], + 'items':[]}"); + var s2 = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.replace", Ref = "1", Value = newBlock }); + + Assert.Equal(19m, s2!.Sums.VatByRate["19"]); // unchanged + Assert.Equal(7m, s2.Sums.VatByRate["7"]); // recomputed from new line + Assert.Equal(250m, s2.Sums.TotalNet); + Assert.Equal(26m, s2.Sums.TotalVat); + } + + // ── BuildSetDisplay reflects both set-pricing modes (mirrors the PDF's InvoiceSetPricing) ── + // SetPayload()'s header item is still unconverted (own total_net == 0, as delivered by + // fds__prepInvoice) — until the set-item switch (ApplyItemSetPrice) actually gives it its own + // price, InvoiceSetPricing.Build must not apply the chosen setmode yet: header stays blank and + // each member keeps showing its own individual price. + [Theory] + [InlineData("setprice")] + [InlineData("setonly")] + public void BuildState_SetDisplay_UnconvertedSet_HeaderBlankMembersIndividuallyPriced(string setmode) + { + var (svc, _, _) = NewService(); + var payload = SetPayload(); + payload["admin"]!["setmode"] = setmode; + var s = svc.OpenFromPayload(payload, "user1"); + + var state = JObject.FromObject(svc.BuildState(s)); + var setDisplay = (JObject)state["setDisplay"]!; + + Assert.False(setDisplay["1"]!["p"]!.Value()); // header not priced yet + Assert.True(setDisplay["2"]!["p"]!.Value()); // member 2 keeps its own price + Assert.True(setDisplay["3"]!["p"]!.Value()); // member 3 keeps its own price + } + + // Once converted (ApplyItemSetPrice has given the header its own price), the chosen setmode + // takes effect: SetPrice blanks the members (still present in the map); SetOnly drops them + // from the map entirely. + [Theory] + [InlineData("setprice", true, false)] // header priced, member 2 blank + [InlineData("setonly", true, false)] // header priced; member 2 dropped entirely (absent from map) + public void BuildState_SetDisplay_ConvertedSet_ReflectsSetmodeForHeaderAndMember(string setmode, bool headerShown, bool memberShown) + { + var (svc, _, _) = NewService(); + var payload = SetPayload(); + payload["admin"]!["setmode"] = setmode; + var s = svc.OpenFromPayload(payload, "user1"); + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "item.setprice", Ref = "1" }); // convert the set first + + var state = JObject.FromObject(svc.BuildState(s)); + var setDisplay = (JObject)state["setDisplay"]!; + + Assert.Equal(headerShown, setDisplay["1"]!["p"]!.Value()); + if (setmode == "setonly") + Assert.False(setDisplay.ContainsKey("2")); // member removed from the display entirely + else + Assert.Equal(memberShown, setDisplay["2"]!["p"]!.Value()); + } + + // ── Full lifecycle recalculation: several edits of different kinds land in one consistent recompute ── + [Fact] + public void ApplyPatch_FullEditSequence_TextReorderSetPriceAndTaxToggle_EndsConsistent() + { + var (svc, _, _) = NewService(); + var s = svc.OpenFromPayload(TwoBlockPayload(), "user1"); + + // 1) text change + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "title", Value = JToken.FromObject("Endabrechnung") }); + // 2) reorder sections + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "block.order", Value = JArray.Parse("['2','1']") }); + // 3) toggle reverse-charge on, then off again (settings roundtrip) + svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "p13b", Value = JToken.FromObject(true) }); + var final = svc.ApplyPatch(s.Token, new InvoiceDraftDelta { Target = "p13b", Value = JToken.FromObject(false) }); + + Assert.NotNull(final); + Assert.Equal("Endabrechnung", final!.New["invoicetitle"]!.Value()); + Assert.Equal(new[] { "2", "1" }, final.Req.Select(b => b["Id"]!.Value()).ToArray()); + Assert.Equal("1", ((JObject)((JArray)((JObject)final.Req[0])["itm"]!)[0])["p"]!.ToString()); // renumbered after reorder + Assert.Equal(130m, final.Sums.TotalNet); // totals stable across the whole sequence + Assert.Equal(24.7m, final.Sums.TotalVat); // VAT restored after the toggle roundtrip + Assert.Equal(4, final.History.Count); + Assert.DoesNotContain(final.ValidationMessages, m => m.Severity == "error"); + } +} diff --git a/Fuchs.Tests/InvoiceOptionsTests.cs b/Fuchs.Tests/InvoiceOptionsTests.cs index d6f91c5..35a68de 100644 --- a/Fuchs.Tests/InvoiceOptionsTests.cs +++ b/Fuchs.Tests/InvoiceOptionsTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Fuchs.intranet; @@ -31,16 +31,25 @@ public class InvoiceOptionsTests => Assert.Equal("", InvoiceOptionsFor(new { type = "r" })); [Fact] - public void DefaultSetPrice_OmitsToken() - => Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "setprice" })); + public void ExplicitDefaultSetPrice_EmitsToken() + // An explicit choice of the default mode must still be persisted — otherwise it is + // indistinguishable from an invoice that was never switched to set-pricing at all + // (the editor relies on this to hide the "Set-Preisanzeige" menu entry once chosen). + => Assert.Equal("setmode:setprice", InvoiceOptionsFor(new { type = "r", setmode = "setprice" })); [Theory] - [InlineData("itemprices", "setmode:itemprices")] [InlineData("setonly", "setmode:setonly")] - [InlineData("ITEMPRICES", "setmode:itemprices")] // case-insensitive + [InlineData("SETONLY", "setmode:setonly")] // case-insensitive public void SetMode_EmitsToken(string mode, string expected) => Assert.Equal(expected, InvoiceOptionsFor(new { type = "r", setmode = mode })); + [Fact] + public void RemovedItemPricesMode_OmitsToken() + // "itemprices" was a valid mode before the button/mode was removed. Any invoice options + // still carrying the stale value (or a client re-posting it) must not be persisted as a + // recognized token — it is treated the same as an unknown/garbage mode. + => Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "itemprices" })); + [Fact] public void UnknownSetMode_OmitsToken() => Assert.Equal("", InvoiceOptionsFor(new { type = "r", setmode = "garbage" })); @@ -103,9 +112,11 @@ public class InvoiceOptionsTests public void ModeFromInvoiceOptions_RoundTripsBackendEmission() { // The token this side emits must parse back to the same mode on the PDF side. - Assert.Equal(SetDisplayMode.ItemPrices, - InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", setmode = "itemprices" }))); Assert.Equal(SetDisplayMode.SetOnly, InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", p13b = true, setmode = "setonly" }))); + // A removed mode never round-trips as itself — it is never persisted, so reading it back + // always yields the default SetPrice. + Assert.Equal(SetDisplayMode.SetPrice, + InvoiceSetPricing.ModeFromInvoiceOptions(InvoiceOptionsFor(new { type = "r", setmode = "itemprices" }))); } } diff --git a/Fuchs.Tests/InvoiceSetPricingTests.cs b/Fuchs.Tests/InvoiceSetPricingTests.cs index bb61669..f8a6e42 100644 --- a/Fuchs.Tests/InvoiceSetPricingTests.cs +++ b/Fuchs.Tests/InvoiceSetPricingTests.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Fuchs.intranet; using Xunit; @@ -40,11 +40,11 @@ public class InvoiceSetPricingTests // ── Mode parsing ──────────────────────────────────────────────────────── [Theory] - [InlineData("itemprices", SetDisplayMode.ItemPrices)] - [InlineData("items", SetDisplayMode.ItemPrices)] [InlineData("setonly", SetDisplayMode.SetOnly)] + [InlineData("set_only", SetDisplayMode.SetOnly)] [InlineData("setprice", SetDisplayMode.SetPrice)] [InlineData("", SetDisplayMode.SetPrice)] + [InlineData("itemprices", SetDisplayMode.SetPrice)] // removed mode — falls back to default, never throws [InlineData("garbage", SetDisplayMode.SetPrice)] public void ParseMode_Works(string raw, SetDisplayMode expected) => Assert.Equal(expected, InvoiceSetPricing.ParseMode(raw)); @@ -52,10 +52,12 @@ public class InvoiceSetPricingTests [Fact] public void ModeFromInvoiceOptions_ReadsToken() { - Assert.Equal(SetDisplayMode.ItemPrices, InvoiceSetPricing.ModeFromInvoiceOptions("§13b,setmode:itemprices")); - Assert.Equal(SetDisplayMode.SetOnly, InvoiceSetPricing.ModeFromInvoiceOptions("setmode:setonly")); - Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions("§13b")); // default - Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions(null)); + Assert.Equal(SetDisplayMode.SetOnly, InvoiceSetPricing.ModeFromInvoiceOptions("§13b,setmode:setonly")); + Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions("§13b")); // default + Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions(null)); + // A stale/removed "itemprices" token (e.g. from an invoice created before the mode was + // dropped) must not throw — it degrades gracefully to the default SetPrice mode. + Assert.Equal(SetDisplayMode.SetPrice, InvoiceSetPricing.ModeFromInvoiceOptions("setmode:itemprices")); } [Fact] @@ -85,22 +87,6 @@ public class InvoiceSetPricingTests Assert.Equal(50.00m, lines[3].TotalNet); } - // ── ItemPrices: members priced, set header blank ────────────────────────── - [Fact] - public void Build_ItemPrices_MembersPricedHeaderBlank() - { - var lines = InvoiceSetPricing.Build(Sample(), SetDisplayMode.ItemPrices); - - Assert.Equal(4, lines.Count); - Assert.True(lines[0].IsSetHeader); - Assert.False(lines[0].ShowPrice); // set header is just a title now - Assert.True(lines[1].ShowPrice); - Assert.Equal(600.00m, lines[1].TotalNet); - Assert.True(lines[2].ShowPrice); - Assert.Equal(400.00m, lines[2].TotalNet); - Assert.True(lines[3].ShowPrice); // standalone - } - // ── SetOnly: members removed ────────────────────────────────────────────── [Fact] public void Build_SetOnly_RemovesMembers() @@ -115,18 +101,45 @@ public class InvoiceSetPricingTests Assert.Equal("Anfahrt", lines[1].Title); } - // ── Set price falls back to sum of members when header total is 0 ───────── + // ── Unconverted set (header total still 0): shown blank, members individually priced ── [Fact] - public void Build_HeaderTotalZero_UsesSumOfMembers() + public void Build_HeaderTotalZero_UnconvertedSet_HeaderBlankMembersPriced() { var items = new List> { - SetHeader("7", "Set ohne Preis"), // total 0 + SetHeader("7", "Set ohne Preis"), // total 0 — not yet converted Member("7", "A", "120.00", "120.00"), Member("7", "B", "80.00", "80.00") }; var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetPrice); - Assert.Equal(200.00m, lines[0].TotalNet); // 120 + 80 + + Assert.Equal(3, lines.Count); + Assert.True(lines[0].IsSetHeader); + Assert.False(lines[0].ShowPrice); // header not priced yet + Assert.Equal(0m, lines[0].TotalNet); + + Assert.True(lines[1].ShowPrice); // members keep their own price + Assert.Equal(120.00m, lines[1].TotalNet); + Assert.True(lines[2].ShowPrice); + Assert.Equal(80.00m, lines[2].TotalNet); + } + + // ── SetOnly on an unconverted set also just passes items through unchanged ── + [Fact] + public void Build_HeaderTotalZero_UnconvertedSet_SetOnlyModeStillPassesThrough() + { + var items = new List> + { + SetHeader("7", "Set ohne Preis"), + Member("7", "A", "120.00", "120.00"), + Member("7", "B", "80.00", "80.00") + }; + var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetOnly); + + Assert.Equal(3, lines.Count); // members not dropped before conversion + Assert.False(lines[0].ShowPrice); + Assert.True(lines[1].ShowPrice); + Assert.True(lines[2].ShowPrice); } // ── No sets: pass-through unchanged ─────────────────────────────────────── @@ -164,7 +177,7 @@ public class InvoiceSetPricingTests } [Fact] - public void Build_TextLine_AsSetMember_NoPriceEvenInItemPrices() + public void Build_TextLine_AsSetMember_NoPriceInSetPriceMode() { var items = new List> { @@ -172,10 +185,10 @@ public class InvoiceSetPricingTests new() { ["type"] = "title", ["setId"] = "10", ["title"] = "Hinweis", ["total_net"] = "" }, Member("10", "Waschbecken", "600.00", "600.00") }; - var lines = InvoiceSetPricing.Build(items, SetDisplayMode.ItemPrices); + var lines = InvoiceSetPricing.Build(items, SetDisplayMode.SetPrice); var note = lines.First(l => l.Title == "Hinweis"); - Assert.False(note.ShowPrice); // text member stays blank - Assert.True(lines.First(l => l.Title == "Waschbecken").ShowPrice); + Assert.False(note.ShowPrice); // text member stays blank (already blank in SetPrice mode) + Assert.False(lines.First(l => l.Title == "Waschbecken").ShowPrice); // members blank in SetPrice mode too } // ── Set price equals sum of member prices across modes (no double counting) ─ diff --git a/Fuchs.Tests/ProcessWebComServiceTests.cs b/Fuchs.Tests/ProcessWebComServiceTests.cs index ce94512..28d2248 100644 --- a/Fuchs.Tests/ProcessWebComServiceTests.cs +++ b/Fuchs.Tests/ProcessWebComServiceTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.Metrics; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading; @@ -48,6 +49,12 @@ public class ProcessWebComServiceTests } } + private sealed class ThrowingHandler : HttpMessageHandler + { + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + => throw new HttpRequestException("simulated transport failure"); + } + private sealed class StubHttpClientFactory : IHttpClientFactory { private readonly HttpMessageHandler _handler; @@ -55,6 +62,41 @@ public class ProcessWebComServiceTests public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false); } + /// + /// Captures what would be written to fds__logEmail without hitting a database, by + /// overriding the audit-log write. Lets tests assert that every send path is logged and that + /// failures carry the service response / exception detail. + /// + private sealed class CapturingComService : ProcessWebComService + { + public readonly List<(bool success, List log)> Entries = new(); + + public CapturingComService( + IOptions settings, + IOptions emailSettings, + IHttpClientFactory httpClientFactory) + : base(NullLogger.Instance, intranet: null!, settings, emailSettings, httpClientFactory) + { + } + + protected override Task WriteAuditLogAsync(string reference, string guid, string config, + DateTime sent, bool success, IEnumerable errors) + { + Entries.Add((success, errors.ToList())); + return Task.CompletedTask; + } + } + + private static CapturingComService CreateCapturing(HttpMessageHandler handler, bool enabled = true, string? overrideRecipient = null) + { + var settings = Options.Create(new ProcessWebComSettings + { + Enabled = enabled, BaseUrl = "https://mailer.test", AccountId = "acct", Token = "tok" + }); + var emailSettings = Options.Create(new FuchsEmailSettings { OverrideRecipient = overrideRecipient }); + return new CapturingComService(settings, emailSettings, new StubHttpClientFactory(handler)); + } + private static ProcessWebComService CreateService(StubHandler handler, bool enabled = true, string? overrideRecipient = null) { var settings = Options.Create(new ProcessWebComSettings @@ -129,6 +171,102 @@ public class ProcessWebComServiceTests Assert.Equal(0, handler.CallCount); } + // ── Audit logging: every attempt is logged; failures carry the service response ─ + [Fact] + public async Task SendEmailAsync_Success_LogsSuccessfulAttempt() + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK)); + + await svc.SendEmailAsync("inv_log_ok", "S", "

x

", "kunde@example.de", "Kunde"); + + var entry = Assert.Single(svc.Entries); + Assert.True(entry.success); + } + + [Fact] + public async Task SendEmailAsync_ApiError_LogsFailureIncludingServiceResponse() + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.InternalServerError, "mailer rejected: quota exceeded")); + + bool result = await svc.SendEmailAsync("inv_log_api", "S", "

x

", "kunde@example.de", "Kunde"); + + Assert.False(result); + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.Contains("mailer rejected: quota exceeded")); // raw service response + Assert.Contains(entry.log, l => l.Contains("500")); // HTTP status + } + + [Fact] + public async Task SendEmailAsync_TransportException_LogsFailureIncludingExceptionDetail() + { + var svc = CreateCapturing(new ThrowingHandler()); + + bool result = await svc.SendEmailAsync("inv_log_ex", "S", "

x

", "kunde@example.de", "Kunde"); + + Assert.False(result); + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.Contains("Exception while sending")); + Assert.Contains(entry.log, l => l.Contains("simulated transport failure")); + } + + [Theory] + [InlineData("not-an-email")] + [InlineData("")] + public async Task SendEmailAsync_InvalidEmail_StillLogsFailedAttempt(string badEmail) + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK)); + + bool result = await svc.SendEmailAsync("inv_log_bad", "S", "

x

", badEmail, "Kunde"); + + Assert.False(result); + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.Contains("Invalid recipient email address")); + } + + [Fact] + public async Task SendEmailAsync_Disabled_LogsFailedAttempt() + { + var svc = CreateCapturing(new StubHandler(HttpStatusCode.OK), enabled: false); + + await svc.SendEmailAsync("inv_log_dis", "S", "

x

", "kunde@example.de", "Kunde"); + + var entry = Assert.Single(svc.Entries); + Assert.False(entry.success); + Assert.Contains(entry.log, l => l.ToLowerInvariant().Contains("disabled")); + } + + // ── Audit-log parameters honour the NOT NULL columns of fds__emaillog ────── + [Fact] + public void BuildAuditLogParameters_FailedAttempt_NeverPassesNullForNotNullColumns() + { + // fds__emaillog.config and .DateSent are NOT NULL; a failed/unset send must not send NULL + // (regression: it previously did, so every audit write failed and no email was ever logged). + var pl = ProcessWebComService.BuildAuditLogParameters( + "admin_test", guid: "", config: "", sent: default, success: false, errors: new[] { "err" }); + + object? config = pl.Single(p => p.ParameterName == "@config").Value; + object? dateSent = pl.Single(p => p.ParameterName == "@DateSent").Value; + + Assert.Equal("", config); + Assert.NotEqual(DBNull.Value, config); + Assert.IsType(dateSent); + Assert.NotEqual((object)DBNull.Value, dateSent); + } + + [Fact] + public void BuildAuditLogParameters_SuccessfulSend_KeepsProvidedSentTimestamp() + { + var when = new DateTime(2026, 7, 16, 12, 0, 0, DateTimeKind.Utc); + + var pl = ProcessWebComService.BuildAuditLogParameters( + "inv_1", guid: "g1", config: "", sent: when, success: true, errors: Array.Empty()); + + Assert.Equal(when, pl.Single(p => p.ParameterName == "@DateSent").Value); + } + // ── Dev/test recipient override safety net ───────────────────────────────── [Fact] public async Task SendEmailAsync_OverrideRecipientSet_RedirectsToOverrideAddress() @@ -141,7 +279,7 @@ public class ProcessWebComServiceTests Assert.True(result); Assert.Equal(1, handler.CallCount); var json = JObject.Parse(handler.LastRequestBody!); - Assert.Equal("dev-inbox@example.test", json["recipient"]!.ToString()); + Assert.Equal("dev-inbox@example.test", json["communication"]!["to"]!.ToString()); } [Fact] @@ -153,7 +291,7 @@ public class ProcessWebComServiceTests await svc.SendEmailAsync("inv_ov2", "Rechnung 123", "

hi

", "realcustomer@example.de", "Kunde"); var json = JObject.Parse(handler.LastRequestBody!); - string subject = json["subject"]!.ToString(); + string subject = json["communication"]!["subject"]!.ToString(); Assert.Contains("realcustomer@example.de", subject); Assert.Contains("Rechnung 123", subject); } @@ -169,7 +307,7 @@ public class ProcessWebComServiceTests Assert.True(result); Assert.Equal(1, handler.CallCount); var json = JObject.Parse(handler.LastRequestBody!); - Assert.Equal("dev-inbox@example.test", json["recipient"]!.ToString()); + Assert.Equal("dev-inbox@example.test", json["communication"]!["to"]!.ToString()); } [Fact] @@ -181,7 +319,7 @@ public class ProcessWebComServiceTests await svc.SendEmailAsync("inv_ov4", "Subject", "

hi

", "realcustomer@example.de", "Kunde"); var json = JObject.Parse(handler.LastRequestBody!); - Assert.Equal("realcustomer@example.de", json["recipient"]!.ToString()); + Assert.Equal("realcustomer@example.de", json["communication"]!["to"]!.ToString()); } // ── Override enforcement across every real appsettings*.json environment ─── @@ -194,9 +332,9 @@ public class ProcessWebComServiceTests // hand-typed literal - so a future edit that silently breaks the override key // or its value in any appsettings*.json file would fail this test. // - // Note: this API has a single "recipient" field - there is no distinct + // Note: this API has a single "communication.to" field - there is no distinct // to/cc/bcc concept anywhere in the codebase (see ProcessWebComService. - // SendEmailAsync / payload.recipient). "to/cc/bcc cleared" is therefore fully + // SendEmailAsync / communication.to). "to/cc/bcc cleared" is therefore fully // satisfied by asserting that field no longer carries the original recipient // once an override is configured. public static IEnumerable AppSettingsEnvironments() @@ -236,7 +374,7 @@ public class ProcessWebComServiceTests Assert.True(result); Assert.Equal(1, handler.CallCount); var json = JObject.Parse(handler.LastRequestBody!); - string sentRecipient = json["recipient"]!.ToString(); + string sentRecipient = json["communication"]!["to"]!.ToString(); if (!string.IsNullOrWhiteSpace(overrideRecipient)) { @@ -266,7 +404,7 @@ public class ProcessWebComServiceTests Assert.True(result); Assert.NotNull(handler.LastRequestBody); var json = JObject.Parse(handler.LastRequestBody!); - var att = (JArray)json["attachments"]!; + var att = (JArray)json["communication"]!["attachments"]!; Assert.Single(att); Assert.Equal("Rechnung.pdf", att[0]!["filename"]!.ToString()); Assert.Equal("application/pdf", att[0]!["mimeType"]!.ToString()); diff --git a/Fuchs.Tests/ReminderDraftCalculatorTests.cs b/Fuchs.Tests/ReminderDraftCalculatorTests.cs new file mode 100644 index 0000000..21bd9eb --- /dev/null +++ b/Fuchs.Tests/ReminderDraftCalculatorTests.cs @@ -0,0 +1,92 @@ +using Fuchs.intranet; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Exhaustively exercises the pure reminder-draft aggregation/validation (ADR 0006, +/// the reminder mirror of ). Being static/pure, +/// the open-amount math and the plausibility checks are unit-testable without a DB. +/// +public class ReminderDraftCalculatorTests +{ + private static ReminderDraftSession Session(string newJson) => + new() { New = JObject.Parse(newJson) }; + + [Theory] + [InlineData("{'amount':119,'amount_payed':0}", 119, 0, 119)] + [InlineData("{'amount':119,'amount_payed':20}", 119, 20, 99)] + [InlineData("{'amount':'119,50','amount_payed':'19,50'}", 119.50, 19.50, 100)] // German decimals + [InlineData("{'amount':'100.00','amount_payed':'40.00'}", 100, 40, 60)] // invariant decimals + [InlineData("{}", 0, 0, 0)] // missing → 0 + [InlineData("{'amount':50,'amount_payed':80}", 50, 80, -30)] // overpaid → negative + public void RecomputeTotals_ComputesOpenAmount(string newJson, double total, double payed, double open) + { + var s = Session(newJson); + ReminderDraftCalculator.RecomputeTotals(s); + Assert.Equal((decimal)total, s.Sums.AmountTotal); + Assert.Equal((decimal)payed, s.Sums.AmountPayed); + Assert.Equal((decimal)open, s.Sums.AmountOpen); + } + + [Fact] + public void Validate_EmptyEmail_Warns() + { + var s = Session("{'amount':119,'invoiceaddress':'Weg 1','subject':'X'}"); + ReminderDraftCalculator.RecomputeTotals(s); + ReminderDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "email" && m.Severity == "warning"); + } + + [Theory] + [InlineData("bad")] + [InlineData("no-at-sign.de")] + [InlineData("trailing@dot.")] + public void Validate_InvalidEmail_Errors(string email) + { + var s = Session($"{{'amount':119,'invoiceemail':'{email}','invoiceaddress':'Weg 1','subject':'X'}}"); + ReminderDraftCalculator.RecomputeTotals(s); + ReminderDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "email" && m.Severity == "error"); + } + + [Fact] + public void Validate_ValidEmail_NoEmailMessage() + { + var s = Session("{'amount':119,'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','subject':'X'}"); + ReminderDraftCalculator.RecomputeTotals(s); + ReminderDraftCalculator.Validate(s); + Assert.DoesNotContain(s.ValidationMessages, m => m.Field == "email"); + } + + [Fact] + public void Validate_EmptyAddressAndSubject_Warn() + { + var s = Session("{'amount':119,'invoiceemail':'a@b.de'}"); + ReminderDraftCalculator.RecomputeTotals(s); + ReminderDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "address" && m.Severity == "warning"); + Assert.Contains(s.ValidationMessages, m => m.Field == "subject" && m.Severity == "warning"); + } + + [Theory] + [InlineData("{'amount':0,'amount_payed':0,'invoiceemail':'a@b.de','invoiceaddress':'W','subject':'X'}")] + [InlineData("{'amount':50,'amount_payed':80,'invoiceemail':'a@b.de','invoiceaddress':'W','subject':'X'}")] + public void Validate_NonPositiveOpenAmount_Warns(string newJson) + { + var s = Session(newJson); + ReminderDraftCalculator.RecomputeTotals(s); + ReminderDraftCalculator.Validate(s); + Assert.Contains(s.ValidationMessages, m => m.Field == "amount" && m.Severity == "warning"); + } + + [Fact] + public void Validate_HealthyDraft_HasNoMessages() + { + var s = Session("{'amount':119,'amount_payed':0,'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','subject':'Zahlungserinnerung'}"); + ReminderDraftCalculator.RecomputeTotals(s); + ReminderDraftCalculator.Validate(s); + Assert.Empty(s.ValidationMessages); + } +} diff --git a/Fuchs.Tests/ReminderDraftServiceTests.cs b/Fuchs.Tests/ReminderDraftServiceTests.cs new file mode 100644 index 0000000..f846db5 --- /dev/null +++ b/Fuchs.Tests/ReminderDraftServiceTests.cs @@ -0,0 +1,307 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Fuchs.intranet; +using Fuchs.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using MigraDoc.DocumentObjectModel; +using Newtonsoft.Json.Linq; +using OCORE.security; +using Xunit; +using static OCORE.OCORE_dictionaries; + +namespace Fuchs.Tests; + +/// +/// Exercises the reminder draft edit orchestrator's pure paths (open/patch/history/flush) +/// without a database — the reminder mirror of , +/// proving the backend-authoritative model behaves correctly at the service seam (ADR 0006). +/// +public class ReminderDraftServiceTests +{ + /// Captures the reminder handed to registration and returns it with a fake DB id — no SQL. + private sealed class FakeReminderService : IReminderService + { + public FdsReminderData? Registered; + public bool? LastChange; + public FdsReminderData? PreviewReminder; + public bool? PreviewDraft; + + public Task RegisterReminderAsync(FdsReminderData reminder, bool change, string remId, string userAccountId, DatabaseSecurity dbSec) + { + Registered = reminder; + LastChange = change; + reminder.ReminderRegistration = new GenericObjectDictionary(new System.Collections.Generic.Dictionary { ["Id"] = "REM42" }); + return Task.FromResult(reminder); + } + public Document GenerateReminderPdf(FdsReminderData reminder, bool draft) { PreviewReminder = reminder; PreviewDraft = draft; return new Document(); } + public Task LoadReminderAsync(string id, string u, DatabaseSecurity s) => throw new NotSupportedException(); + public Task RenderReminderPdfBytesAsync(FdsReminderData r, bool d) => throw new NotSupportedException(); + public Task StoreReminderDocumentFileAsync(FdsReminderData r, bool d, string u, DatabaseSecurity s) => throw new NotSupportedException(); + public Task GetReminderFileAsync(FdsReminderData r, bool d, fds.IFdsMfr m, string u, DatabaseSecurity s) => throw new NotSupportedException(); + public Task<(System.IO.FileInfo? file, byte[]? content)> GetStoredFileAsync(string id, string u, DatabaseSecurity s) => throw new NotSupportedException(); + } + + private static (ReminderDraftEditService svc, FakeReminderService rem) NewService() + { + var cache = new ReminderDraftCache(new ConfigurationBuilder().Build()); + var rem = new FakeReminderService(); + var svc = new ReminderDraftEditService(cache, rem, NullLogger.Instance); + return (svc, rem); + } + + private static JObject Payload() => JObject.Parse(@"{ + 'rem':{'invid':'INV5','type':'R','invoiceid':'R2026-1','invoicedate':'2026-06-01'}, + 'new':{'invoiceemail':'a@b.de','invoiceaddress':'Weg 1','subject':'Zahlungserinnerung','amount':119,'amount_payed':0} + }"); + + [Fact] + public void OpenFromPayload_SeedsSessionAndComputesOpenAmount() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + Assert.False(string.IsNullOrEmpty(s.Token)); + Assert.Equal(0, s.Version); + Assert.Equal(119m, s.Sums.AmountTotal); + Assert.Equal(119m, s.Sums.AmountOpen); + } + + [Fact] + public void ApplyPatch_Email_MutatesBumpsVersionAndRecordsHistory() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "email", Value = JToken.FromObject("neu@x.de") }); + + Assert.NotNull(s2); + Assert.Equal(1, s2!.Version); + Assert.Equal("neu@x.de", s2.New["invoiceemail"]!.Value()); + var h = Assert.Single(s2.History); + Assert.Equal("email", h.Target); + Assert.Equal("a@b.de", h.OldValue); + Assert.Equal("neu@x.de", h.NewValue); + } + + [Fact] + public void ApplyPatch_Amount_RecomputesOpenAmount() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "amount", Value = JToken.FromObject(200) }); + + Assert.Equal(200m, s2!.Sums.AmountTotal); + Assert.Equal(200m, s2.Sums.AmountOpen); + } + + [Fact] + public void ApplyPatch_AmountPayed_RecomputesOpenAmount() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "amount_payed", Value = JToken.FromObject(19) }); + + Assert.Equal(100m, s2!.Sums.AmountOpen); // 119 - 19 + } + + [Fact] + public void ApplyPatch_AmountFromGermanString_NormalisesToInvariant() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "amount", Value = JToken.FromObject("249,90") }); + + Assert.Equal("249.90", s2!.New["amount"]!.Value()); // stored invariant + Assert.Equal(249.90m, s2.Sums.AmountTotal); + } + + [Fact] + public void ApplyPatch_UnknownToken_ReturnsNull() + { + var (svc, _) = NewService(); + Assert.Null(svc.ApplyPatch("ghost", new ReminderDraftDelta { Target = "email", Value = JToken.FromObject("x@y.de") })); + } + + [Fact] + public void ApplyPatch_UnknownTarget_IsNoOp() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "nonsense", Value = JToken.FromObject("x") }); + + Assert.Equal(0, s2!.Version); + Assert.Empty(s2.History); + } + + [Theory] + [InlineData("subject", "subject")] + [InlineData("address", "invoiceaddress")] + [InlineData("text", "text")] + public void ApplyPatch_ScalarFieldDeltas_UpdateNew(string target, string newKey) + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = target, Value = JToken.FromObject("X-VALUE") }); + + Assert.Equal("X-VALUE", s2!.New[newKey]!.Value()); + } + + [Theory] + [InlineData("subject", "subject")] + [InlineData("email", "invoiceemail")] + public void ApplyPatch_ScalarField_StripsHtmlWrapper(string target, string newKey) + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = target, Value = JToken.FromObject("

clean me

") }); + + Assert.Equal("clean me", s2!.New[newKey]!.Value()); + Assert.DoesNotContain("<", s2.New[newKey]!.Value()); + } + + [Fact] + public void ApplyPatch_Address_MultilineHtml_KeepsLineBreaks() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta + { + Target = "address", + Value = JToken.FromObject("

Firma AG

Weg 1
40000 Düsseldorf

") + }); + + Assert.Equal("Firma AG\nWeg 1\n40000 Düsseldorf", s2!.New["invoiceaddress"]!.Value()); + } + + [Fact] + public void ApplyPatch_Contact_BuildsCustomValuesJson() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var s2 = svc.ApplyPatch(s.Token, new ReminderDraftDelta + { + Target = "contact", + Value = JObject.Parse(@"{'name':'Max Mustermann','email':'max@kunde.de'}") + }); + + var cv = JObject.Parse(s2!.New["CustomValues"]!.Value()!); + Assert.Equal("Max Mustermann", cv["contactName"]!.Value()); + Assert.Equal("max@kunde.de", cv["contactEmail"]!.Value()); + } + + [Fact] + public void ApplyPatch_MultipleEdits_AccumulateHistoryInOrder() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "email", Value = JToken.FromObject("a1@x.de") }); + svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "subject", Value = JToken.FromObject("Mahnung 2") }); + var s3 = svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "amount", Value = JToken.FromObject(200) }); + + Assert.Equal(3, s3!.Version); + Assert.Equal(new[] { "email", "subject", "amount" }, s3.History.Select(h => h.Target).ToArray()); + Assert.Equal(new[] { 1, 2, 3 }, s3.History.Select(h => h.Version).ToArray()); + } + + [Fact] + public void BuildState_ExposesPayloadSumsValidationAndVersion() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + svc.ApplyPatch(s.Token, new ReminderDraftDelta { Target = "amount_payed", Value = JToken.FromObject(19) }); + + var state = JObject.FromObject(svc.BuildState(svc.Get(s.Token)!)); + + Assert.Equal(1, state["version"]!.Value()); + Assert.Equal(119m, state["sums"]!["amount_total"]!.Value()); + Assert.Equal(100m, state["sums"]!["amount_open"]!.Value()); + Assert.Equal(1, state["historyCount"]!.Value()); + Assert.NotNull(state["validation"]); + } + + [Fact] + public async Task FlushToDbAsync_RegistersAndSetsRemId_CreatePath() + { + var (svc, rem) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var result = await svc.FlushToDbAsync(s.Token, "user1", null!); + + Assert.NotNull(result); + Assert.Equal("REM42", result!.Id); + Assert.False(rem.LastChange); // new draft (no prior RemId) → create + Assert.Equal("REM42", svc.Get(s.Token)!.RemId); + // the email/subject the editor set must reach registration + Assert.Equal("a@b.de", rem.Registered!.RawInvoiceEmail); + Assert.Equal("Zahlungserinnerung", rem.Registered!.NewValues!.getString("subject")); + Assert.Equal("INV5", rem.Registered!.RawInvId); + } + + [Fact] + public async Task FlushToDbAsync_ExistingRemId_UpdatePath() + { + var (svc, rem) = NewService(); + var payload = Payload(); + payload["remid"] = "REM7"; + var s = svc.OpenFromPayload(payload, "user1"); + + await svc.FlushToDbAsync(s.Token, "user1", null!); + + Assert.True(rem.LastChange); // prior RemId → update path + } + + [Fact] + public void RenderPreview_SynthesizesDraftRegistrationFromSession() + { + var (svc, rem) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + var doc = svc.RenderPreview(s.Token); + + Assert.NotNull(doc); + Assert.True(rem.PreviewDraft); + Assert.True(rem.PreviewReminder!.IsDraft); + var reg = rem.PreviewReminder!.ReminderRegistration!; + Assert.Equal("Zahlungserinnerung", reg.getString("subject")); + Assert.Equal("Weg 1", reg.getString("SendToAddress")); + Assert.Equal("a@b.de", reg.getString("SendToEmail")); + Assert.Equal("R2026-1", reg.getString("InvoiceId")); + // the synthesised single-invoice row the reminder table renders + Assert.Single(rem.PreviewReminder!.ReminderItems); + } + + [Fact] + public void RenderPreview_UnknownToken_ReturnsNull() + { + var (svc, _) = NewService(); + Assert.Null(svc.RenderPreview("ghost")); + } + + [Fact] + public void GetHistory_UnknownToken_IsEmpty() + { + var (svc, _) = NewService(); + Assert.Empty(svc.GetHistory("ghost")); + } + + [Fact] + public void Close_RemovesSession_ThenReportsFalse() + { + var (svc, _) = NewService(); + var s = svc.OpenFromPayload(Payload(), "user1"); + + Assert.True(svc.Close(s.Token)); + Assert.Null(svc.Get(s.Token)); + Assert.False(svc.Close(s.Token)); + } +} diff --git a/Fuchs.Tests/StartupSelfTestServiceTests.cs b/Fuchs.Tests/StartupSelfTestServiceTests.cs index ff0199a..b12c587 100644 --- a/Fuchs.Tests/StartupSelfTestServiceTests.cs +++ b/Fuchs.Tests/StartupSelfTestServiceTests.cs @@ -205,6 +205,54 @@ public class StartupSelfTestServiceTests Assert.Equal(1, service.MailerCalls); } + [Fact] + public async Task RunOnce_Disabled_PublishesNotRunReport() + { + var reporter = new StartupCheckReporter(); + using var service = new TestableStartupSelfTestService( + new ServiceCollection().BuildServiceProvider(), + CreateConfiguration(), + Options.Create(CreateSettings(enabled: false)), + NullLogger.Instance, + reporter); + + await service.RunForTestAsync(CancellationToken.None); + + Assert.NotNull(reporter.Latest); + Assert.False(reporter.Latest!.Ran); + } + + [Fact] + public async Task RunOnce_Enabled_PublishesReportWithPerCheckResults() + { + var reporter = new StartupCheckReporter(); + using var service = new TestableStartupSelfTestService( + new ServiceCollection().BuildServiceProvider(), + CreateConfiguration(), + Options.Create(CreateSettings(enabled: true, checkKeyVault: true, checkDatabase: true, checkMfr: false, sendStartupEmail: true, startupRecipient: "ops@example.test", checkPdfLicense: false)), + NullLogger.Instance, + reporter) + { + KeyVaultResult = true, + DatabaseResult = false, + MailerResult = true, + }; + + await service.RunForTestAsync(CancellationToken.None); + + var report = reporter.Latest; + Assert.NotNull(report); + Assert.True(report!.Ran); + Assert.NotNull(report.CompletedUtc); + var kv = Assert.Single(report.Items, i => i.Name == "KeyVault"); + Assert.True(kv.Enabled && kv.Ok); + var db = Assert.Single(report.Items, i => i.Name == "Database"); + Assert.True(db.Enabled); + Assert.False(db.Ok); // intentionally-failing path + var mfr = Assert.Single(report.Items, i => i.Name == "MFR"); + Assert.False(mfr.Enabled); // disabled check reported as not-enabled + } + [Fact] public async Task ExecuteAsync_ProbeThrows_ServiceDoesNotThrowAndContinuesRemainingChecks() { @@ -250,8 +298,9 @@ public class StartupSelfTestServiceTests IServiceProvider serviceProvider, IConfiguration configuration, IOptions settings, - Microsoft.Extensions.Logging.ILogger logger) - : base(serviceProvider, configuration, settings, logger) + Microsoft.Extensions.Logging.ILogger logger, + StartupCheckReporter? reporter = null) + : base(serviceProvider, configuration, settings, logger, reporter) { } diff --git a/Fuchs.Tests/SystemStatusServiceTests.cs b/Fuchs.Tests/SystemStatusServiceTests.cs new file mode 100644 index 0000000..b02b954 --- /dev/null +++ b/Fuchs.Tests/SystemStatusServiceTests.cs @@ -0,0 +1,465 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading; +using System.Threading.Tasks; +using Fuchs.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Tests for the Admin module's read-only diagnostics service. Covers the passive config +/// snapshot (SQL server parsing, email override reflection, presence flags), each probe's +/// disabled/unconfigured/ok/error outcomes where deterministically reachable without live +/// infrastructure, the test-email pipeline (success/failure/validation + HTML-encoding + +/// override reporting), and the emitted probe telemetry. +/// +public class SystemStatusServiceTests +{ + private const string FuchsMeterName = "Fuchs.Intranet"; + + private static IConfiguration Config(Dictionary? overrides = null) + { + var dict = new Dictionary + { + ["ConnectionStrings:fuchs_fds_ConnectionString"] = + "Data Source=SQLHOST,1433;Initial Catalog=site_fuchs_test;User ID=fuchs_app;password='secret';", + ["ConnectionStrings:AzureBlobStorage_ConnectionString"] = "DefaultEndpointsProtocol=https;AccountName=acct;AccountKey=key==;", + ["Fuchs:IsTestDeployment"] = "true", + ["Fds:MFR_host"] = "portal.mobilefieldreport.com", + ["Fds:MFR_UserName"] = "mfruser", + ["Fds:SyncEnabled"] = "true", + ["SecretManagement:VaultUri"] = "https://vault.example/", + ["SecretManagement:AppName"] = "fuchs", + ["SecretManagement:ManagedSecretKeys:0"] = "Fuchs--Mailer--Token", + ["SecretManagement:ManagedSecretKeys:1"] = "ConnectionStrings--fuchs-fds-password", + }; + if (overrides != null) + foreach (var kv in overrides) dict[kv.Key] = kv.Value; + return new ConfigurationBuilder().AddInMemoryCollection(dict).Build(); + } + + private static SystemStatusService Build( + IConfiguration? config = null, + Mock? blob = null, + Mock? mfr = null, + Mock? com = null, + ProcessWebComSettings? mailer = null, + FuchsEmailSettings? email = null, + AzureBlobStorageSettings? blobSettings = null, + IServiceProvider? provider = null, + StartupCheckReporter? startupChecks = null) + { + var env = new Mock(); + env.SetupGet(e => e.EnvironmentName).Returns("Development"); + + return new SystemStatusService( + config ?? Config(), + env.Object, + provider ?? new ServiceCollection().BuildServiceProvider(), + (blob ?? new Mock()).Object, + (mfr ?? new Mock()).Object, + (com ?? new Mock()).Object, + Options.Create(mailer ?? new ProcessWebComSettings { Enabled = true, Token = "tok", BaseUrl = "https://api.example", AccountId = "acc", ServerId = "srv" }), + Options.Create(email ?? new FuchsEmailSettings()), + Options.Create(blobSettings ?? new AzureBlobStorageSettings { Enabled = true }), + startupChecks ?? new StartupCheckReporter(), + NullLogger.Instance); + } + + // ── GetInfo (passive snapshot) ───────────────────────────────────────────── + [Fact] + public void GetInfo_ParsesConnectionStringAndReflectsConfiguration() + { + var svc = Build(); + + SystemInfoSnapshot info = svc.GetInfo(); + + Assert.Equal("Development", info.Environment); + Assert.True(info.IsTestDeployment); + Assert.True(info.Database.Configured); + Assert.Equal("SQLHOST,1433", info.Database.Server); + Assert.Equal("site_fuchs_test", info.Database.Catalog); + Assert.Equal("fuchs_app", info.Database.UserId); // login only, never the password + Assert.True(info.Mfr.CredentialsConfigured); + Assert.Equal("portal.mobilefieldreport.com", info.Mfr.Host); + Assert.True(info.Mfr.SyncEnabled); + Assert.Equal("fuchs", info.KeyVault.AppName); + Assert.Equal(2, info.KeyVault.ManagedSecretCount); + Assert.False(info.KeyVault.ClientRegistered); // empty provider + } + + [Fact] + public void GetInfo_NeverLeaksDatabasePassword() + { + var svc = Build(); + + SystemInfoSnapshot info = svc.GetInfo(); + + Assert.DoesNotContain("secret", info.Database.UserId ?? ""); + Assert.DoesNotContain("secret", info.Database.Server ?? ""); + Assert.DoesNotContain("secret", info.Database.Catalog ?? ""); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void GetInfo_EmailOverride_ReflectedOnlyWhenConfigured(bool overrideSet) + { + var email = new FuchsEmailSettings { OverrideRecipient = overrideSet ? "safety@example.test" : "" }; + var svc = Build(email: email); + + SystemInfoSnapshot info = svc.GetInfo(); + + Assert.Equal(overrideSet, info.Email.OverrideActive); + Assert.Equal(overrideSet ? "safety@example.test" : null, info.Email.OverrideRecipient); + Assert.True(info.Email.MailerEnabled); // Build() default enables the mailer + Assert.True(info.Email.TokenConfigured); + } + + [Fact] + public void GetInfo_StartupChecks_NullWhenServiceHasNotRun() + { + var svc = Build(startupChecks: new StartupCheckReporter()); // nothing set yet + + Assert.Null(svc.GetInfo().StartupChecks); + } + + [Fact] + public void GetInfo_StartupChecks_ReflectsReporterContents() + { + var reporter = new StartupCheckReporter(); + reporter.Set(new StartupCheckReport + { + Ran = true, + CompletedUtc = DateTimeOffset.UtcNow, + MachineName = "BUILD-HOST", + Items = + [ + new StartupCheckItem { Name = "Database", Enabled = true, Ok = true }, + new StartupCheckItem { Name = "Mailer", Enabled = false, Ok = false }, + ], + }); + var svc = Build(startupChecks: reporter); + + var report = svc.GetInfo().StartupChecks; + + Assert.NotNull(report); + Assert.True(report!.Ran); + Assert.Equal("BUILD-HOST", report.MachineName); + Assert.Equal(2, report.Items.Count); + Assert.Contains(report.Items, i => i.Name == "Database" && i.Enabled && i.Ok); + } + + [Fact] + public void GetInfo_DatabaseUnconfigured_WhenConnectionStringMissing() + { + var config = Config(new() { ["ConnectionStrings:fuchs_fds_ConnectionString"] = "" }); + var svc = Build(config); + + Assert.False(svc.GetInfo().Database.Configured); + } + + // ── Blob probe ───────────────────────────────────────────────────────────── + [Fact] + public async Task ProbeBlob_Disabled_ReturnsDisabled() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity { Enabled = false }); + var svc = Build(blob: blob); + + var r = await svc.ProbeAsync("blob"); + + Assert.Equal("blob", r.Component); + Assert.Equal("disabled", r.Status); + Assert.False(r.Ok); + } + + [Fact] + public async Task ProbeBlob_EnabledButUnconfigured_ReturnsUnconfigured() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity { Enabled = true, Configured = false, Detail = "keine Verbindungszeichenfolge" }); + var svc = Build(blob: blob); + + var r = await svc.ProbeAsync("blob"); + + Assert.Equal("unconfigured", r.Status); + Assert.False(r.Ok); + } + + [Fact] + public async Task ProbeBlob_Reachable_ReturnsOkWithAccountDetail() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity { Enabled = true, Configured = true, Reachable = true, AccountName = "acct", Detail = "StorageV2 / Standard_LRS" }); + var svc = Build(blob: blob); + + var r = await svc.ProbeAsync("blob"); + + Assert.Equal("ok", r.Status); + Assert.True(r.Ok); + Assert.Contains("acct", r.Detail); + } + + [Fact] + public async Task ProbeBlob_Reachable_ReportsFileCountsPerContainerAsMetrics() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity + { + Enabled = true, Configured = true, Reachable = true, AccountName = "acct", + Detail = "StorageV2 / Standard_LRS", + Containers = new List + { + new() { Name = "dev-fuchs-invoices", Exists = true, FileCount = 42 }, + new() { Name = "dev-fuchs-reminders", Exists = false, FileCount = 0 }, + }, + }); + var svc = Build(blob: blob); + + var r = await svc.ProbeAsync("blob"); + + Assert.Equal("ok", r.Status); + Assert.NotNull(r.Metrics); + Assert.Equal(2, r.Metrics!.Count); + var inv = Assert.Single(r.Metrics, m => m.Label == "dev-fuchs-invoices"); + Assert.Equal("42 Dateien", inv.Value); + var rem = Assert.Single(r.Metrics, m => m.Label == "dev-fuchs-reminders"); + Assert.Equal("nicht vorhanden", rem.Value); + Assert.Contains("42 Dateien gesamt", r.Message); // total across containers + } + + [Fact] + public async Task ProbeBlob_FileCountUnavailable_ReportsMetricWithoutFailing() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity + { + Enabled = true, Configured = true, Reachable = true, AccountName = "acct", + Containers = new List + { + new() { Name = "dev-fuchs-invoices", Exists = true, FileCount = -1 }, + }, + }); + var svc = Build(blob: blob); + + var r = await svc.ProbeAsync("blob"); + + Assert.Equal("ok", r.Status); + var inv = Assert.Single(r.Metrics!); + Assert.Equal("Anzahl nicht ermittelbar", inv.Value); + } + + [Fact] + public async Task ProbeBlob_ConfiguredButUnreachable_ReturnsError() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity { Enabled = true, Configured = true, Reachable = false, AccountName = "acct", Detail = "403 Forbidden" }); + var svc = Build(blob: blob); + + var r = await svc.ProbeAsync("blob"); + + Assert.Equal("error", r.Status); + Assert.False(r.Ok); + } + + // ── Key Vault probe ──────────────────────────────────────────────────────── + [Fact] + public async Task ProbeKeyVault_ClientNotRegistered_ReturnsUnconfigured() + { + var svc = Build(); // empty service provider → no SecretClient + + var r = await svc.ProbeAsync("keyvault"); + + Assert.Equal("keyvault", r.Component); + Assert.Equal("unconfigured", r.Status); + Assert.False(r.Ok); + } + + [Fact] + public async Task ProbeKeyVault_NoManagedKeys_ReturnsUnconfigured() + { + var config = Config(new() + { + ["SecretManagement:ManagedSecretKeys:0"] = null, + ["SecretManagement:ManagedSecretKeys:1"] = null, + }); + var svc = Build(config); + + var r = await svc.ProbeAsync("keyvault"); + + Assert.Equal("unconfigured", r.Status); + } + + // ── MFR probe ────────────────────────────────────────────────────────────── + [Fact] + public async Task ProbeMfr_FactoryThrows_ReturnsError() + { + var mfr = new Mock(); + mfr.Setup(f => f.Create()).Throws(new InvalidOperationException("no credentials")); + var svc = Build(mfr: mfr); + + var r = await svc.ProbeAsync("mfr"); + + Assert.Equal("mfr", r.Component); + Assert.Equal("error", r.Status); + Assert.False(r.Ok); + Assert.Equal("no credentials", r.Detail); + } + + // ── Database probe (unconfigured is deterministic without a live server) ──── + [Fact] + public async Task ProbeDatabase_NoConnectionString_ReturnsUnconfigured() + { + var config = Config(new() { ["ConnectionStrings:fuchs_fds_ConnectionString"] = "" }); + var svc = Build(config); + + var r = await svc.ProbeAsync("database"); + + Assert.Equal("database", r.Component); + Assert.Equal("unconfigured", r.Status); + Assert.False(r.Ok); + } + + // ── Unknown component ────────────────────────────────────────────────────── + [Fact] + public async Task ProbeAsync_UnknownComponent_ReturnsError() + { + var svc = Build(); + + var r = await svc.ProbeAsync("does-not-exist"); + + Assert.Equal("error", r.Status); + Assert.False(r.Ok); + } + + [Fact] + public async Task ProbeAllAsync_ReturnsOneResultPerComponent() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity { Enabled = false }); + var mfr = new Mock(); + mfr.Setup(f => f.Create()).Throws(new InvalidOperationException("x")); + var config = Config(new() { ["ConnectionStrings:fuchs_fds_ConnectionString"] = "" }); + var svc = Build(config, blob: blob, mfr: mfr); + + var results = await svc.ProbeAllAsync(); + + Assert.Equal(svc.ProbeComponents.Count, results.Count); + foreach (var comp in svc.ProbeComponents) + Assert.Contains(results, r => r.Component == comp); + Assert.All(results, r => Assert.True(r.DurationMs >= 0)); + } + + [Fact] + public async Task ProbeAsync_EmitsProbeTelemetry() + { + var blob = new Mock(); + blob.Setup(b => b.CheckConnectivityAsync(It.IsAny())) + .ReturnsAsync(new BlobConnectivity { Enabled = false }); + var svc = Build(blob: blob); + + long delta = 0; + using var listener = new MeterListener + { + InstrumentPublished = (inst, l) => + { + if (inst.Meter.Name == FuchsMeterName && inst.Name == "fuchs.systemstatus.probes") + l.EnableMeasurementEvents(inst); + } + }; + listener.SetMeasurementEventCallback((_, value, _, _) => Interlocked.Add(ref delta, value)); + listener.Start(); + + await svc.ProbeAsync("blob"); + + Assert.True(delta >= 1, "fuchs.systemstatus.probes counter should increment per probe."); + } + + // ── Test email ───────────────────────────────────────────────────────────── + [Theory] + [InlineData("", "subj", "body")] + [InlineData("to@example.test", "", "body")] + public async Task SendTestEmailAsync_MissingRequiredFields_DoesNotSend(string to, string subject, string body) + { + var com = new Mock(); + var svc = Build(com: com); + + var r = await svc.SendTestEmailAsync(to, subject, body); + + Assert.False(r.Sent); + com.Verify(c => c.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny>()), Times.Never); + } + + [Fact] + public async Task SendTestEmailAsync_Success_SendsHtmlEncodedBodyToRecipient() + { + string? capturedHtml = null, capturedTo = null, capturedSubject = null; + var com = new Mock(); + com.Setup(c => c.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny?>())) + .Callback?>( + (_, subj, html, to, _, _) => { capturedSubject = subj; capturedHtml = html; capturedTo = to; }) + .ReturnsAsync(true); + var svc = Build(com: com); + + var r = await svc.SendTestEmailAsync("dest@example.test", "Betreff", "Zeile1 diff --git a/Fuchs/appsettings.Development.json b/Fuchs/appsettings.Development.json index 56b599e..1966d3a 100644 --- a/Fuchs/appsettings.Development.json +++ b/Fuchs/appsettings.Development.json @@ -26,16 +26,22 @@ "CheckMfr": false, "CheckPdfLicense": true }, + "Mailer": { + "Enabled": true + }, "Email": { - "OverrideRecipient": "service@emails.processweb.de" + "OverrideRecipient": "info@processweb.de" }, "AzureStorage": { - "Enabled": false, + "Enabled": true, "InvoiceContainer": "dev-fuchs-invoices", "ReminderContainer": "dev-fuchs-reminders" } }, "Fds": { + "SyncEnabled": false, + "ExecutionFrequency_Minutes": 15, + "DebugDetails": true, "MFR_host": "portal.mobilefieldreport.com", "MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com", "MFR_Password": "0oT4G3H2" diff --git a/Fuchs/appsettings.json b/Fuchs/appsettings.json index d546cb8..5e6ce0a 100644 --- a/Fuchs/appsettings.json +++ b/Fuchs/appsettings.json @@ -32,6 +32,7 @@ "AzureBlobStorage_ConnectionString": "MANAGED_BY_KEYVAULT" }, "Fuchs": { + "IsTestDeployment": true, "ocms_guid": "00094b8f-a822-4e9c-b627-87802f93fca8", "ocms_valid_locales_csv": "de,en", "ocms_default_locale": "de", @@ -44,10 +45,11 @@ "BaseUrl": "https://api.processweb.de", "AccountId": "82d87114-c8c3-4d33-95e5-4c781a9229ab", "Token": "MANAGED_BY_KEYVAULT", - "Enabled": false + "ServerId": "87AAB12E-29E0-47AB-8DD1-5F5D49B4AC2E", + "Enabled": true }, "Email": { - "OverrideRecipient": "" + "OverrideRecipient": "info@processweb.de" }, "StartupChecks": { "Enabled": false, @@ -59,9 +61,9 @@ "StartupEmailRecipientName": "Startup Monitor" }, "AzureStorage": { - "Enabled": false, - "InvoiceContainer": "fuchs-invoices", - "ReminderContainer": "fuchs-reminders", + "Enabled": true, + "InvoiceContainer": "dev-fuchs-invoices", + "ReminderContainer": "dev-fuchs-reminders", "MetadataFields": [ "Id", "Version", "InvoiceId", "InvoiceTitle", "InvId", "DocumentName", "file_guid" ] }, "Telemetry": { @@ -70,6 +72,9 @@ } }, "Fds": { + "SyncEnabled": true, + "ExecutionFrequency_Minutes": 15, + "DebugDetails": false, "MFR_host": "portal.mobilefieldreport.com", "MFR_UserName": "MANAGED_BY_KEYVAULT", "MFR_Password": "MANAGED_BY_KEYVAULT" diff --git a/Fuchs/bdlconfig.json b/Fuchs/bdlconfig.json index 1b40a86..4788cc7 100644 --- a/Fuchs/bdlconfig.json +++ b/Fuchs/bdlconfig.json @@ -151,5 +151,24 @@ "js/intranet/modules/fis.bam.scss" ], "minify": { "enabled": true } + }, + { + "context": "intranet:admin", + "outputFileName": "wwwroot/web/fis.admin.de.js", + "inputFiles": [ + "js/intranet/modules/fis.admin_txt_de.js", + "js/intranet/modules/fis.admin.js" + ], + "minify": { "enabled": true } + }, + { + "context": "intranet:admin", + "outputFileName": "wwwroot/web/fis.admin.min.css", + "inputFiles": [ + "css/intranet/oci_variables.scss", + "css/intranet/fis_variables.scss", + "js/intranet/modules/fis.admin.scss" + ], + "minify": { "enabled": true } } ] diff --git a/Fuchs/code/7z.dll b/Fuchs/code/7z.dll deleted file mode 100644 index b32d7bf..0000000 Binary files a/Fuchs/code/7z.dll and /dev/null differ diff --git a/Fuchs/code/FdsInvoiceData.cs b/Fuchs/code/FdsInvoiceData.cs index bf4bb14..30cbc49 100644 --- a/Fuchs/code/FdsInvoiceData.cs +++ b/Fuchs/code/FdsInvoiceData.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using Microsoft.Data.SqlClient; using Newtonsoft.Json.Linq; using static OCORE.OCORE_dictionaries; @@ -19,7 +19,7 @@ public class FdsInvoiceData public GenericObjectDictionary? Admin { get; private set; } public GenericObjectDictionary? NewValues { get; private set; } public GenericObjectDictionary? Sms { get; private set; } - public List>? Req { get; private set; } + public List>? Req { get; internal set; } public GenericObjectDictionary? InvoiceRegistration { get; internal set; } public bool IsDraft { get; internal set; } = true; @@ -46,9 +46,25 @@ public class FdsInvoiceData get { var result = new List>(); + foreach (var block in InvoiceBlocks) result.AddRange(block.Items); + return result; + } + } + + /// + /// The service-request groups as they should render on the invoice: each block carries its + /// heading (the section title the editor shows) and its line items. The PDF renders a heading + /// row per block followed by that block's items, so the online editor and the PDF stay in sync. + /// + public List InvoiceBlocks + { + get + { + var result = new List(); if (Req == null) return result; foreach (var req in Req) { + var items = new List>(); if (req.TryGetValue("items", out var itmsObj)) { IEnumerable>? itms = @@ -56,8 +72,12 @@ public class FdsInvoiceData ?? (itmsObj is JArray ja ? ja.ToObject>>() : null); - if (itms != null) result.AddRange(itms); + if (itms != null) items.AddRange(itms); } + string heading = ""; + if (req.TryGetValue("text", out var th) && th != null) heading = th.ToString() ?? ""; + if (heading.Length == 0 && req.TryGetValue("nme", out var nh) && nh != null) heading = nh.ToString() ?? ""; + result.Add(new InvoiceBlock { Heading = heading, Items = items }); } return result; } @@ -151,7 +171,11 @@ public class FdsInvoiceData var tokens = new List(); if (Admin?.no("p13b", false) is true) tokens.Add("§13b"); string setmode = (Admin?.nz("setmode") ?? "").Trim().ToLowerInvariant(); - if (setmode is "itemprices" or "setonly") tokens.Add("setmode:" + setmode); + // Persist any explicitly-chosen mode, including the default "setprice" — otherwise an + // explicit user choice of the default is indistinguishable from an invoice that was never + // switched to set-pricing at all (needed so the editor can hide the "Set-Preisanzeige" + // menu entry once a mode has been chosen; see InvoiceDraftEditService.BuildInvoiceOptions). + if (setmode is "setprice" or "setonly") tokens.Add("setmode:" + setmode); return string.Join(",", tokens); } @@ -196,3 +220,12 @@ public class FdsInvoiceData return double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0; } } + +/// A service-request group as it renders on the invoice: a heading plus its line items. +public sealed class InvoiceBlock +{ + /// The section heading (editor's text/nme); empty when the group has none. + public string Heading { get; init; } = ""; + /// The group's line items (the editor's items contract). + public List> Items { get; init; } = new(); +} diff --git a/Fuchs/code/FuchsPdf.cs b/Fuchs/code/FuchsPdf.cs index d8b7f66..d42d29c 100644 --- a/Fuchs/code/FuchsPdf.cs +++ b/Fuchs/code/FuchsPdf.cs @@ -112,21 +112,40 @@ public static class FuchsPdf "Kreissparkasse D\u00fcsseldorf", "IBAN: DE52 3015 0200 0002 0914 78", "BIC: WELADED1KSD", "Stadtsparkasse D\u00fcsseldorf", "IBAN: DE76 3005 0110 0045 0148 00", "BIC: DUSSDEDDXXX" }; + // Reminder greeting/intro, keyed by the actual reminder type (f = Zahlungserinnerung, + // m = Mahnung, l = letzte außergerichtliche Mahnung) — the single source of truth for + // both the PDF (ApplyReminder) and the online editor (via BuildReminderNotes). public Dictionary ReminderTexts_before { get; } = new() { ["f"] = new[] { "Sehr geehrte Damen und Herren,", - "wir erlauben uns, Sie auf die nachfolgende offene Rechnung aufmerksam zu machen." + "ein Mahnschreiben sollte kurz, freundlich und erfolgreich sein. Kurz ist es, freundlich sowieso; ob es auch erfolgreich ist, h\u00e4ngt von Ihnen ab." }, - ["s"] = new[] { + ["m"] = new[] { "Sehr geehrte Damen und Herren,", - "trotz unserer Ersten Mahnung haben wir bis heute noch keinen Zahlungseingang feststellen k\u00f6nnen. " + - "Wir bitten Sie, den offenen Betrag inkl. der Mahnzinsen und Mahnkosten umgehend auf unser Konto zu \u00fcberweisen." + "nun m\u00fcssen wir Sie noch einmal anschreiben.", + "Wahrscheinlich haben Sie triftige Gr\u00fcnde daf\u00fcr, warum Sie die Zahlung unserer Forderung nicht vornehmen und auch nicht auf unsere Mahnung reagieren. Sollten wir dar\u00fcber nicht einmal sprechen?", + "Bitte nehmen Sie umgehend in dieser Sache mit uns Kontakt auf." }, - ["l"] = Array.Empty() + ["l"] = new[] { + "Sehr geehrte Damen und Herren,", + "Eine DRITTE MAHNUNG zu erhalten bereitet Ihnen bestimmt ebenso wenig Freude wie uns, sie zu verschicken. Leider haben wir auf unsere zweite Mahnung noch keine Antwort von Ihnen erhalten.", + "Wir bitten Sie, den offenen Betrag innerhalb der n\u00e4chsten 7 Werktage nach Erhalt dieses Schreibens zu begleichen. Nach Ablauf dieser Frist erfolgt keine weitere Mahnung mehr.", + "Sollte die Forderung bis dahin nicht beglichen sein, er\u00f6ffnen wir das gerichtliche Mahnverfahren. Sollten Sie die Rechnung inzwischen beglichen haben, so betrachten Sie bitte dieses Schreiben als gegenstandslos." + } + }; + + /// Extra closing sentence appended for reminder type m (Mahnung) — f's closing is generated + /// dynamically (amount/IBAN + "gegenstandslos"); l gets no extra closing note. + public Dictionary ReminderClosingExtra { get; } = new() + { + ["m"] = "Um Ihnen zus\u00e4tzliche Kosten f\u00fcr weitere Mahnungen zu ersparen, bitten wir Sie nunmehr um die \u00dcberweisung des noch zu zahlenden Gesamtbetrages inklusive der ggf. bereits f\u00e4lligen Mahnzinsen und Mahngeb\u00fchren innerhalb von einer Woche." }; } + /// One notice/explanatory paragraph, as rendered both in the PDF and the online editor. + public sealed record PdfNote(string Text, string Style = "InvoiceNotes"); + // ── Currency / parse helpers ─────────────────────────────────────────────── public static string Currency(object? input, string returnobject = "?") { @@ -137,6 +156,34 @@ public static class FuchsPdf public static string TranslatePaymentTerm(string pt) => pt.Replace("wd", " Werktagen").Replace("d", " Tagen").Replace("wk", " Wochen").ne("10 Tagen"); + /// + /// Maps one editor item-contract entry to a display line, mirroring the online editor: the + /// item's own price/total is shown, a set header is emphasised, and free-text/heading lines + /// (type text/title) show neither a price nor a position number. This is the flat + /// (non-collapsing) rendering used for every set mode except the explicit setonly. + /// + private static InvoiceSetLine MapItemToLine(Dictionary i) + { + string type = i.nz("type", "").ToLowerInvariant(); + bool isText = type is "text" or "title"; + ParseDec(i.no("price_net", 0), out decimal price); + ParseDec(i.no("total_net", 0), out decimal total); + // ADR 0009: a null/absent total_net means "no price" — render an empty cell (not 0,00 €), + // distinct from a genuine 0. This is how block-mode (Set mit Preis) nulled members print. + bool hasPrice = i.TryGetValue("total_net", out var tv) && tv != null; + return new InvoiceSetLine + { + Title = i.nz("title", ""), + Desc = i.nz("desc", ""), + Qty = i.nz("qty", ""), + PriceNet = price, + TotalNet = total, + ShowPrice = !isText && hasPrice, + Numbered = !isText, // numbered like the editor: every line except free-text/heading, even price-blanked set members + IsSetHeader = type == "set" + }; + } + /// /// Parses a numeric value coming from JSON deserialization (long/double), SQL /// (decimal), or an already-invariant numeric string. Numeric CLR types are @@ -505,7 +552,7 @@ public static class FuchsPdf ParagraphFormat ClonePf() { var pf = doc.Styles["Normal"]!.ParagraphFormat.Clone(); - pf.Borders.Distance = 3; + pf.Borders.Distance = 0; pf.Alignment = ParagraphAlignment.Left; return pf; } @@ -533,126 +580,21 @@ public static class FuchsPdf AddStyle(doc, "InvoiceNotes_ucb", "InvoiceNotes", s => s.Font = new Font(tgtFont, 9) { Bold = true }); } - // ── ApplyInvoice ────────────────────────────────────────────────────────── - public static void ApplyInvoice(Document doc, FdsTextBlocks tb, FdsInvoiceData inv, bool draft = false) + /// + /// Builds the ordered list of standard invoice notice paragraphs (ported from legacy + /// fuchs_fds_pdf.vb) — pure data, no MigraDoc dependency, so it can be reused by both + /// and the online editor's draft view-state (ADR: notice texts + /// are backend-authoritative — the editor renders exactly what the PDF will show). + /// + public static List BuildInvoiceNotes(FdsTextBlocks tb, FdsInvoiceData inv, bool p13b) { - string invoiceType = inv.InvoiceType; - bool p13b = inv.InvoiceRegistration?.getString("InvoiceOptions") - .Split(',').Contains("§13b") ?? false; - Apply_Invoice_Styles(doc); - - var sec = doc.Sections.Cast
().First(); - sec.PageSetup.RightMargin = cm(1.5); - - // Title - { - var p = sec.AddParagraph(); - p.Style = "SubjectBig"; - p.Format.SpaceBefore = cm(8.65); - p.AddText($"{inv.InvoiceTitle} Nr. {inv.InvoiceId}"); - p.Format.SpaceAfter = cm(0.5); - } - - // Provision location - if (inv.ProvisionLocation.Length > 0) - { - var p = sec.AddParagraph(); - p.Style = "AddressBox"; - p.Format.Font.Size = 10; - p.Format.LineSpacingRule = LineSpacingRule.Exactly; - p.Format.LineSpacing = 12; - p.AddFormattedText(tb.ProvisionLoc_Label, TextFormat.Bold); - p.AddLineBreak(); - foreach (string t in inv.ProvisionLocation) { p.AddText(t); p.AddLineBreak(); } - } - sec.AddParagraph().Format.SpaceBefore = cm(0.7); - - // Invoice items table - var tbl = sec.AddTable(); - tbl.Style = "Table"; - tbl.Format.SpaceBefore = mm(1); tbl.Format.SpaceAfter = mm(1); - tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25; - tbl.Borders.Left.Width = 0.5; tbl.Borders.Right.Width = 0.5; - tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6); - tbl.Rows.HeightRule = RowHeightRule.AtLeast; - - double pageW = sec.PageSetup.PageWidth.Centimeter - - sec.PageSetup.LeftMargin.Centimeter - - sec.PageSetup.RightMargin.Centimeter; - double[] colWidths = { pageW * 0.06, pageW * 0.42, pageW * 0.12, pageW * 0.18, pageW * 0.22 }; - foreach (double w in colWidths) tbl.AddColumn(cm(w)); - - // Header row - string[] headers = { "Pos.", "Bezeichnung", "Menge", "Einzelpreis", "Gesamtpreis" }; - var hRow = tbl.AddRow(); - hRow.Shading = GrayShading(); - for (int i = 0; i < headers.Length; i++) - { - var p = hRow.Cells[i].AddParagraph(); p.Style = "TblCell_Head"; - p.AddFormattedText(headers[i], TextFormat.Bold); - hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left; - } - - // Data rows — resolved through the set-display mode (see InvoiceSetPricing). - // For invoices without sets this passes items through unchanged; for sets it - // emits set header + members per the chosen mode, blanking price cells where - // a line should show no price. Totals come from the registration balance, so - // the mode is purely presentational. - var setMode = InvoiceSetPricing.ModeFromInvoiceOptions(inv.InvoiceRegistration?.getString("InvoiceOptions")); - int pos = 1; - foreach (var line in InvoiceSetPricing.Build(inv.InvoiceItems, setMode)) - { - var row = tbl.AddRow(); - row.HeightRule = RowHeightRule.Auto; - row.Cells[0].AddParagraph(pos.ToString()).Style = "TblCell_Base"; - var titleCell = row.Cells[1].AddParagraph(); - titleCell.Style = "TblCell_RTitle"; - if (line.IsSetHeader) titleCell.AddFormattedText(line.Title, TextFormat.Bold); - else titleCell.AddText(line.Title); - if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"
{line.Desc}
"); - row.Cells[2].AddParagraph(line.Qty).Style = "TblCell_Base"; - row.Cells[3].AddParagraph(line.ShowPrice ? Currency(line.PriceNet) : "").Style = "TblCell_Base"; - row.Cells[4].AddParagraph(line.ShowPrice ? Currency(line.TotalNet) : "").Style = "TblCell_RSum"; - row.Cells[2].Format.Alignment = ParagraphAlignment.Right; - row.Cells[3].Format.Alignment = ParagraphAlignment.Right; - row.Cells[4].Format.Alignment = ParagraphAlignment.Right; - pos++; - } - - // Totals - void TotalRow(string label, string value, string style = "TblCell_TSum", bool topBorder = false) - { - var r = tbl.AddRow(); r.Shading = LightShading(); - if (topBorder) r.Borders.Top.Width = 0.5; - var lp = r.Cells[3].AddParagraph(); lp.Style = style; lp.AddText(label); - r.Cells[3].Format.Alignment = ParagraphAlignment.Left; - var vp = r.Cells[4].AddParagraph(); vp.Style = style; vp.AddText(value); - r.Cells[4].Format.Alignment = ParagraphAlignment.Right; - } - - ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance_net"), out decimal netSum); - TotalRow("Nettobetrag:", Currency(netSum), "TblCell_TNet", topBorder: true); - - // VAT rows - foreach (var vr in inv.VatRows) - { - string pct = vr.Key; - ParseDec(vr.Value.no("vat_amount", 0), out decimal vatAmt); - TotalRow($"MwSt. {pct}%:", Currency(vatAmt), "TblCell_TVat"); - } - - ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance"), out decimal gross); - TotalRow("Rechnungsbetrag:", Currency(gross), "TblCell_TSum"); - - - // ── Standard invoice notes (ported from legacy fuchs_fds_pdf.vb) ────────── - var reg = inv.InvoiceRegistration; + var notes = new List(); void Note(string text, string style = "InvoiceNotes") { - if (string.IsNullOrEmpty(text)) return; - var np = sec.AddParagraph(); np.Style = style; np.AddText(text); + if (!string.IsNullOrEmpty(text)) notes.Add(new PdfNote(text, style)); } + var reg = inv.InvoiceRegistration; ParseDec(reg?.getItem("InvoiceService"), out decimal invService); string serviceGross = Currency(reg?.getItem("InvoiceService")); @@ -729,8 +671,182 @@ public static class FuchsPdf "die Umwelt zu schützen. Deshalb konzentrieren wir uns auf die Aufforstung von Flächen."); Note("Wir bedanken uns herzlich für Ihren Auftrag."); + return notes; + } + + /// + /// Builds the reminder intro/closing notice paragraphs (greeting + closing bank/amount and + /// "gegenstandslos"/Mahnzinsen sentences), keyed by the actual reminder type. Pure data — no + /// MigraDoc dependency — reused by and the online editor's draft + /// view-state so the editor shows exactly what the PDF will show. + /// + public static (string[] Intro, string[] Closing) BuildReminderNotes(FdsTextBlocks tb, FdsReminderData rem) + { + string rtype = rem.ReminderType; + string[] intro = tb.ReminderTexts_before.TryGetValue(rtype, out var i) ? i : Array.Empty(); + + ParseDec(rem.ReminderRegistration?.getItem("amount_open"), out decimal openTotal); + string terms = TranslatePaymentTerm(rem.ReminderRegistration?.getString("PaymentTerm") ?? ""); + + var closing = new List(); + switch (rtype) + { + case "f": + closing.Add( + $"Bitte überweisen Sie den offenen Betrag von {Currency(openTotal)} innerhalb von {terms} auf unser Konto:\n" + + "IBAN: DE76\u00a03005\u00a00110\u00a00045\u00a00148\u00a000, BIC DUSSSDEDDXXX (Stadtsparkasse Düsseldorf)"); + closing.Add("Sollten Sie den Betrag bereits überwiesen haben, so bitten wir Sie, diese Zahlungserinnerung als gegenstandslos zu betrachten."); + break; + default: + if (tb.ReminderClosingExtra.TryGetValue(rtype, out var extra)) closing.Add(extra); + break; + } + return (intro, closing.ToArray()); + } + + // ── ApplyInvoice ────────────────────────────────────────────────────────── + public static void ApplyInvoice(Document doc, FdsTextBlocks tb, FdsInvoiceData inv, bool draft = false) + { + string invoiceType = inv.InvoiceType; + bool p13b = inv.InvoiceRegistration?.getString("InvoiceOptions") + .Split(',').Contains("§13b") ?? false; + Apply_Invoice_Styles(doc); + + var sec = doc.Sections.Cast
().First(); + sec.PageSetup.RightMargin = cm(1.5); + + // Title + { + var p = sec.AddParagraph(); + p.Style = "SubjectBig"; + p.Format.SpaceBefore = cm(8.65); + p.AddText($"{inv.InvoiceTitle} Nr. {inv.InvoiceId}"); + p.Format.SpaceAfter = cm(0.5); + } + + // Provision location + if (inv.ProvisionLocation.Length > 0) + { + var p = sec.AddParagraph(); + p.Style = "AddressBox"; + p.Format.Font.Size = 10; + p.Format.LineSpacingRule = LineSpacingRule.Exactly; + p.Format.LineSpacing = 12; + p.AddFormattedText(tb.ProvisionLoc_Label, TextFormat.Bold); + p.AddLineBreak(); + foreach (string t in inv.ProvisionLocation) { p.AddText(t); p.AddLineBreak(); } + } + sec.AddParagraph().Format.SpaceBefore = cm(0.7); + + // Invoice items table + var tbl = sec.AddTable(); + tbl.Style = "Table"; + tbl.Format.SpaceBefore = mm(1); tbl.Format.SpaceAfter = mm(1); + tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25; + tbl.Borders.Left.Width = 0.5; tbl.Borders.Right.Width = 0.5; + tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6); + tbl.Rows.HeightRule = RowHeightRule.AtLeast; + + double pageW = sec.PageSetup.PageWidth.Centimeter + - sec.PageSetup.LeftMargin.Centimeter + - sec.PageSetup.RightMargin.Centimeter; + double[] colWidths = { pageW * 0.06, pageW * 0.42, pageW * 0.12, pageW * 0.18, pageW * 0.22 }; + foreach (double w in colWidths) tbl.AddColumn(cm(w)); + + // Header row + string[] headers = { "Pos.", "Bezeichnung", "Menge", "Einzelpreis", "Gesamtpreis" }; + var hRow = tbl.AddRow(); + hRow.Shading = GrayShading(); + for (int i = 0; i < headers.Length; i++) + { + var p = hRow.Cells[i].AddParagraph(); p.Style = "TblCell_Head"; + p.AddFormattedText(headers[i], TextFormat.Bold); + hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left; + } + + // Data rows — grouped by service-request block (see FdsInvoiceData.InvoiceBlocks). + // Each section prints its heading; positions are numbered the same way the editor numbers + // them (every line except free-text/heading lines, including a set header). The chosen + // set-display mode (see INVOICE_SET_PRICING.md) governs how "set" items and their members + // are shown: SetPrice (default) prices the set, blanks members; SetOnly prices the set + // and drops the members entirely. + // Blocks without any set items are unaffected and always render flat. + var setMode = InvoiceSetPricing.ModeFromInvoiceOptions(inv.InvoiceRegistration?.getString("InvoiceOptions")); + int pos = 0; + foreach (var block in inv.InvoiceBlocks) + { + if (!string.IsNullOrWhiteSpace(block.Heading)) + { + var hr = tbl.AddRow(); + hr.HeightRule = RowHeightRule.Auto; + hr.Cells[1].MergeRight = 3; // span Bezeichnung … Gesamtpreis + hr.Cells[1].AddParagraph().WithStyle("TblCell_RTitle").AddFormattedText(block.Heading, TextFormat.Bold); + } + + // Function-1 (SetItmId-linked) sets still collapse via Build; block-mode sets (ADR 0009) + // and plain blocks render flat — their set row is emphasised on its own and nulled + // members map to blank cells (see MapItemToLine). + var lines = InvoiceSetPricing.HasSetMembers(block.Items) + ? InvoiceSetPricing.Build(block.Items, setMode) + : block.Items.Select(MapItemToLine).ToList(); + + foreach (var line in lines) + { + bool numbered = line.Numbered; // every line except free-text/heading (mirrors the editor's RecomputePositions), even price-blanked set members + var row = tbl.AddRow(); + row.HeightRule = RowHeightRule.Auto; + row.Cells[0].AddParagraph(numbered ? (++pos).ToString() : "").Style = "TblCell_Base"; + if (!string.IsNullOrEmpty(line.Title)) // skip the empty paragraph that added a blank line before free text + { + var titleCell = row.Cells[1].AddParagraph(); + titleCell.Style = "TblCell_RTitle"; + if (line.IsSetHeader) titleCell.AddFormattedText(line.Title, TextFormat.Bold); + else titleCell.AddText(line.Title); + } + if (!string.IsNullOrEmpty(line.Desc)) row.Cells[1].AddHtml($"
{line.Desc}
"); + row.Cells[2].AddParagraph(line.Qty).Style = "TblCell_Base"; + row.Cells[3].AddParagraph(line.ShowPrice ? Currency(line.PriceNet) : "").Style = "TblCell_Base"; + row.Cells[4].AddParagraph(line.ShowPrice ? Currency(line.TotalNet) : "").Style = "TblCell_RSum"; + row.Cells[2].Format.Alignment = ParagraphAlignment.Right; + row.Cells[3].Format.Alignment = ParagraphAlignment.Right; + row.Cells[4].Format.Alignment = ParagraphAlignment.Right; + } + } + + // Totals + void TotalRow(string label, string value, string style = "TblCell_TSum", bool topBorder = false) + { + var r = tbl.AddRow(); r.Shading = LightShading(); + if (topBorder) r.Borders.Top.Width = 0.5; + var lp = r.Cells[3].AddParagraph(); lp.Style = style; lp.AddText(label); + r.Cells[3].Format.Alignment = ParagraphAlignment.Left; + var vp = r.Cells[4].AddParagraph(); vp.Style = style; vp.AddText(value); + r.Cells[4].Format.Alignment = ParagraphAlignment.Right; + } + + ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance_net"), out decimal netSum); + TotalRow("Nettobetrag:", Currency(netSum), "TblCell_TNet", topBorder: true); + + // VAT rows + foreach (var vr in inv.VatRows) + { + string pct = vr.Key; + ParseDec(vr.Value.no("vat_amount", 0), out decimal vatAmt); + TotalRow($"MwSt. {pct}%:", Currency(vatAmt), "TblCell_TVat"); + } + + ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance"), out decimal gross); + TotalRow("Rechnungsbetrag:", Currency(gross), "TblCell_TSum"); + + + // ── Standard invoice notes (ported from legacy fuchs_fds_pdf.vb) ────────── + foreach (var note in BuildInvoiceNotes(tb, inv, p13b)) + { + var np = sec.AddParagraph(); np.Style = note.Style; np.AddText(note.Text); + } + // GiroCode payment QR (only on finalized invoices with a positive balance) - ParseDec(reg?.getItem("InvoiceBalance"), out decimal payAmount); + ParseDec(inv.InvoiceRegistration?.getItem("InvoiceBalance"), out decimal payAmount); if (!inv.IsDraft && payAmount > 0 && !string.IsNullOrWhiteSpace(inv.InvoiceId)) AddGirocode(sec, payAmount, $"{inv.InvoiceTitle.ne("Rechnung")} {inv.InvoiceId}"); } @@ -752,31 +868,37 @@ public static class FuchsPdf p.AddText(rem.ReminderTitle.ne("Zahlungserinnerung")); } + var (introLines, closingLines) = BuildReminderNotes(tb, rem); + // Opening text - if (tb.ReminderTexts_before.TryGetValue(rtype, out var intro)) + if (introLines.Length > 0) { var p = sec.AddParagraph(); p.Style = "BodyText"; p.Format.SpaceBefore = cm(0.5); - foreach (string line in intro) { p.AddText(line); p.AddLineBreak(); } + foreach (string line in introLines) { p.AddText(line); p.AddLineBreak(); } } + sec.AddParagraph().Format.SpaceBefore = cm(0.7); - // Reminder items table + // Reminder items table — mirrors the online editor's remHR columns exactly + // (Rechnung, vom, Rechnungsbetrag, bereits bezahlt, noch offen). var tbl = sec.AddTable(); tbl.Style = "Table"; + tbl.Format.SpaceBefore = mm(1); tbl.Format.SpaceAfter = mm(1); tbl.Borders.Color = Colors.White; tbl.Borders.Width = 0.25; + tbl.Borders.Left.Width = 0.5; tbl.Borders.Right.Width = 0.5; tbl.Rows.LeftIndent = 0; tbl.Rows.Height = cm(0.6); tbl.Rows.HeightRule = RowHeightRule.AtLeast; - tbl.Format.SpaceBefore = mm(4); double pageW = sec.PageSetup.PageWidth.Centimeter - sec.PageSetup.LeftMargin.Centimeter - sec.PageSetup.RightMargin.Centimeter; + tbl.AddColumn(cm(pageW * 0.20)); tbl.AddColumn(cm(pageW * 0.15)); - tbl.AddColumn(cm(pageW * 0.45)); - tbl.AddColumn(cm(pageW * 0.20)); - tbl.AddColumn(cm(pageW * 0.20)); + tbl.AddColumn(cm(pageW * 0.22)); + tbl.AddColumn(cm(pageW * 0.21)); + tbl.AddColumn(cm(pageW * 0.22)); - string[] headers = { "Datum", "Bezeichnung", "Betrag", "Offen" }; + string[] headers = { "Rechnung", "vom", "Rechnungsbetrag", "bereits bezahlt", "noch offen" }; var hRow = tbl.AddRow(); hRow.Shading = GrayShading(); for (int i = 0; i < headers.Length; i++) { @@ -785,33 +907,39 @@ public static class FuchsPdf hRow.Cells[i].Format.Alignment = i >= 2 ? ParagraphAlignment.Right : ParagraphAlignment.Left; } - foreach (var itm in rem.ReminderItems) - { - var row = tbl.AddRow(); row.HeightRule = RowHeightRule.Auto; - row.Cells[0].AddParagraph(itm.nz("InvoiceDate", "")); - row.Cells[1].AddParagraph(itm.nz("DocumentName", "").ne(itm.nz("InvoiceTitle", ""))); - row.Cells[2].AddParagraph(Currency(itm.no("InvoiceBalance", 0))); - row.Cells[3].AddParagraph(Currency(itm.no("amount_open", 0))); - row.Cells[2].Format.Alignment = ParagraphAlignment.Right; - row.Cells[3].Format.Alignment = ParagraphAlignment.Right; - } + ParseDec(rem.ReminderRegistration?.getItem("amount"), out decimal amountTotal); + ParseDec(rem.ReminderRegistration?.getItem("amount_payed"), out decimal amountPayed); + ParseDec(rem.ReminderRegistration?.getItem("amount_open"), out decimal openTotal); + string invoiceDate = rem.ReminderRegistration?.getString("InvoiceDate") is { Length: > 0 } d + && DateTime.TryParse(d, out var dd) ? dd.ToString("dd.MM.yy") : ""; + + var row = tbl.AddRow(); row.HeightRule = RowHeightRule.Auto; + row.Cells[0].AddParagraph(rem.InvoiceId); + row.Cells[1].AddParagraph(invoiceDate); + row.Cells[2].AddParagraph(Currency(amountTotal)); + row.Cells[3].AddParagraph(Currency(amountPayed)); + row.Cells[4].AddParagraph(Currency(openTotal)); + for (int i = 2; i <= 4; i++) row.Cells[i].Format.Alignment = ParagraphAlignment.Right; // Open total - ParseDec(rem.ReminderRegistration?.getItem("amount_open"), out decimal openTotal); var sumRow = tbl.AddRow(); sumRow.Shading = LightShading(); sumRow.Borders.Top.Width = 0.5; - sumRow.Cells[2].AddParagraph().AddFormattedText("Offener Betrag:", TextFormat.Bold); - var sv = sumRow.Cells[3].AddParagraph(); + sumRow.Cells[3].AddParagraph().AddFormattedText("Offener Betrag:", TextFormat.Bold); + var sv = sumRow.Cells[4].AddParagraph(); sv.AddFormattedText(Currency(openTotal), TextFormat.Bold); - sumRow.Cells[3].Format.Alignment = ParagraphAlignment.Right; + sumRow.Cells[4].Format.Alignment = ParagraphAlignment.Right; // Closing text + QR code - var closing = sec.AddParagraph(); closing.Style = "BodyText"; - closing.Format.SpaceBefore = cm(0.8); - string terms = TranslatePaymentTerm(rem.ReminderRegistration?.getString("PaymentTerm") ?? ""); - closing.AddText( - $"Bitte \u00fcberweisen Sie den offenen Betrag von {Currency(openTotal)} innerhalb von {terms} auf unser Konto:\n" + - "IBAN: DE76\u00a03005\u00a00110\u00a00045\u00a00148\u00a000, BIC DUSSSDEDDXXX (Stadtsparkasse D\u00fcsseldorf)"); + if (closingLines.Length > 0) + { + var closing = sec.AddParagraph(); closing.Style = "BodyText"; + closing.Format.SpaceBefore = cm(0.8); + for (int i = 0; i < closingLines.Length; i++) + { + if (i > 0) closing.AddLineBreak(); + closing.AddText(closingLines[i]); + } + } // GiroCode payment QR (only on finalized reminders with a positive open amount) if (!rem.IsDraft && openTotal > 0 && !string.IsNullOrWhiteSpace(rem.InvoiceId)) diff --git a/Fuchs/code/InvoiceDraftCalculator.cs b/Fuchs/code/InvoiceDraftCalculator.cs new file mode 100644 index 0000000..9cdc653 --- /dev/null +++ b/Fuchs/code/InvoiceDraftCalculator.cs @@ -0,0 +1,216 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; + +namespace Fuchs.intranet; + +/// +/// Server-side, pure aggregation of an invoice draft's totals/VAT — the authoritative +/// replacement for the browser's invSumUpdate footer math (ADR 0006). The user's +/// requirement is that the sums live in the backend cache, not the frontend. +/// +/// It reads each block's persisted line contract (block.itm = the editor's co +/// objects: vt=net, vv=VAT, vs=service-net, vsv=service-VAT, +/// vat=rate) — exactly the shape the editor already posts and the PDF/persistence +/// already consume — so no line data is re-derived or re-shaped. It then applies the §13b +/// reverse-charge rule and validates. Static/pure, hence exhaustively unit-testable. +/// +public static class InvoiceDraftCalculator +{ + /// + /// Recomputes every line's own net/VAT/service values from its raw quantity (qn), + /// unit price (v) and VAT rate (vat) — the authoritative, server-side port of + /// the former client-side quantChange/setVat multiplication (ADR 0006/0008: the + /// online editor performs no arithmetic at all, not even a single line's net = qty × price). + /// Only lines that actually carry a raw quantity and a positive unit price are recomputed + /// (mirrors quantChange's own guard); lines without both (headings, free text, + /// combined-sum rows, and set members that have been zeroed by + /// 's "Auf Setpreis umstellen" conversion) + /// keep whatever value they already carry, so a set header's synthesised sum is never + /// clobbered by a subsequent recompute. + /// + public static void RecomputeLineValues(InvoiceDraftSession session) + { + foreach (var blockTok in session.Req) + { + if (blockTok is not JObject block || block["itm"] is not JArray lines) continue; + foreach (var lineTok in lines) + { + if (lineTok is not JObject co) continue; + decimal qty = Dec(co["qn"]); + decimal price = Dec(co["v"]); + if (qty <= 0 || price <= 0) continue; // no raw qty/price posted -> leave the value as delivered + + string rate = NormalizeRate(Str(co["vat"])); + decimal vatFactor = rate.Length > 0 && decimal.TryParse(rate, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal r) + ? r / 100m : 0m; + decimal netVal = Math.Round(qty * price, 2, MidpointRounding.AwayFromZero); + decimal vatVal = Math.Round(netVal * vatFactor, 2, MidpointRounding.AwayFromZero); + + co["vt"] = netVal; + co["vv"] = vatVal; + bool isService = string.Equals(Str(co["typ"]), "Service", StringComparison.OrdinalIgnoreCase); + co["vs"] = isService ? netVal : 0; + co["vsv"] = isService ? vatVal : 0; + } + } + } + + /// + /// Aggregates every block's line values into the draft's totals — the port of + /// invSumUpdate's csms accumulation plus §13b (VAT suppressed → gross = net). + /// VAT is grouped by the line's rate string (matching the editor's sms.vat map). + /// + public static void RecomputeTotals(InvoiceDraftSession session) + { + var sums = new InvoiceDraftSums(); + bool p13b = Flag(session.Admin, "p13b"); + + foreach (var blockTok in session.Req) + { + if (blockTok is not JObject block) continue; + decimal blockNet = 0; + string blockId = Str(block["Id"]); + if (block["itm"] is JArray lines) + { + foreach (var lineTok in lines) + { + if (lineTok is not JObject co) continue; + decimal netVal = Dec(co["vt"]); + decimal vatVal = Dec(co["vv"]); + decimal svcNet = Dec(co["vs"]); + decimal svcVat = Dec(co["vsv"]); + + sums.ServiceNet += svcNet; + sums.ServiceVat += svcVat; + sums.TotalNet += netVal; + sums.TotalVat += vatVal; + sums.TotalGross += netVal + vatVal; + blockNet += netVal; + + string rate = NormalizeRate(Str(co["vat"])); + if (rate.Length > 0) + sums.VatByRate[rate] = sums.VatByRate.GetValueOrDefault(rate) + vatVal; + } + } + if (!string.IsNullOrEmpty(blockId)) + sums.NetByBlock[blockId] = sums.NetByBlock.GetValueOrDefault(blockId) + blockNet; + } + + if (p13b) + { + // Reverse-charge: no VAT lines, gross equals net (mirrors invSumUpdate's else-branch). + sums.TotalGross = sums.TotalNet; + sums.TotalVat = 0; + sums.VatByRate.Clear(); + } + + session.Sums = sums; + } + + /// + /// Renumbers the visible line positions authoritatively (the port of the client-side + /// numbering in invSumUpdate): priced lines are numbered sequentially across the whole + /// invoice — matching the PDF's Pos. column — while heading/free-text lines + /// (typ = "text"/"title") carry no number. The result is written onto each line's + /// p field so it flows back to the browser (via the view state) and into the PDF; this + /// keeps the online editor and the PDF preview showing the same position numbers, including + /// after a reorder. + /// + public static void RecomputePositions(InvoiceDraftSession session) + { + int pos = 0; + foreach (var blockTok in session.Req) + { + if (blockTok is not JObject block || block["itm"] is not JArray lines) continue; + foreach (var lineTok in lines) + { + if (lineTok is not JObject co) continue; + string typ = Str(co["typ"]).Trim().ToLowerInvariant(); + bool numbered = typ is not ("text" or "title"); // only headings/free-text carry no number (mirrors invSumUpdate) + co["p"] = numbered ? (JToken)(++pos) : (JToken)""; + } + } + } + + /// + /// Refreshes the draft's plausibility / consistency findings. "error" severity marks + /// issues that should block a clean finalise; "warning" is advisory. Kept in German, + /// user-readable, so the frontend can render them directly. + /// + public static void Validate(InvoiceDraftSession session) + { + session.ValidationMessages.Clear(); + void Add(string field, string sev, string msg) => + session.ValidationMessages.Add(new InvoiceDraftValidationMessage(field, sev, msg)); + + string email = Str(session.New["invoiceemail"]).Trim(); + if (email.Length == 0) + Add("email", "warning", "Es ist keine E-Mail-Adresse hinterlegt — die Rechnung kann nicht per E-Mail versandt werden."); + else if (!IsValidEmail(email)) + Add("email", "error", "Die E-Mail-Adresse ist ungültig."); + + if (Str(session.New["invoiceaddress"]).Trim().Length == 0) + Add("address", "warning", "Es ist keine Rechnungsanschrift hinterlegt."); + + if (!HasAnyItem(session)) + Add("items", "error", "Die Rechnung enthält keine Positionen."); + + if (!Flag(session.Admin, "p13b")) + foreach (var rate in session.Sums.VatByRate.Keys) + if (!IsKnownVatRate(rate)) + Add("vat", "warning", $"Ungewöhnlicher Umsatzsteuersatz: {rate}%."); + + if (session.Sums.TotalGross < 0) + Add("total", "warning", "Der Rechnungsbetrag ist negativ."); + } + + // ── helpers ────────────────────────────────────────────────────────────── + private static bool HasAnyItem(InvoiceDraftSession session) + { + foreach (var blockTok in session.Req) + if (blockTok is JObject block && block["itm"] is JArray lines && lines.Count > 0) + return true; + return false; + } + + /// Parses a JToken to a decimal, tolerating German ("12,50") and invariant ("12.50") strings and "%". + internal static decimal Dec(JToken? token) + { + if (token == null || token.Type == JTokenType.Null) return 0; + if (token.Type is JTokenType.Float or JTokenType.Integer) return token.Value(); + return FuchsPdf.ParseDec(Str(token), out decimal d) ? d : 0; + } + + private static string Str(JToken? token) => + token == null || token.Type == JTokenType.Null ? "" : token.Type == JTokenType.String ? token.Value() ?? "" : token.ToString(); + + private static bool Flag(JObject obj, string key) + { + var t = obj[key]; + if (t == null || t.Type == JTokenType.Null) return false; + if (t.Type == JTokenType.Boolean) return t.Value(); + string s = Str(t).Trim().ToLowerInvariant(); + return s is "1" or "true" or "yes" or "ja" or "on"; + } + + /// Normalises a VAT rate string ("19,0%", "7%", "19") to a canonical numeric string ("19", "7"). + internal static string NormalizeRate(string? raw) + { + string s = (raw ?? "").Replace("%", "").Trim().Replace(',', '.'); + if (s.Length == 0) return ""; + if (!double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out double d) || d == 0) return ""; + return d == Math.Floor(d) + ? ((long)d).ToString(CultureInfo.InvariantCulture) + : d.ToString(CultureInfo.InvariantCulture); + } + + private static bool IsKnownVatRate(string rate) => rate is "0" or "7" or "19"; + + private static bool IsValidEmail(string email) + { + int at = email.IndexOf('@'); + if (at <= 0 || at != email.LastIndexOf('@')) return false; + int dot = email.IndexOf('.', at); + return dot > at + 1 && dot < email.Length - 1; + } +} diff --git a/Fuchs/code/InvoiceDraftSession.cs b/Fuchs/code/InvoiceDraftSession.cs new file mode 100644 index 0000000..727da86 --- /dev/null +++ b/Fuchs/code/InvoiceDraftSession.cs @@ -0,0 +1,111 @@ +using Newtonsoft.Json.Linq; + +namespace Fuchs.intranet; + +/// +/// Server-side, in-memory editing state for a single invoice draft — the +/// authoritative source of truth while a back-office user is editing a draft in +/// the browser (see ADR 0006). The browser is a pure view/input layer: it posts +/// single changes (), the server +/// mutates this session, recomputes totals/VAT (replacing the former client-side +/// invSumUpdate) and validates, then signals the browser to re-fetch. +/// +/// This is a data holder only — all calculation, validation, persistence +/// and rendering live in +/// (mirroring the / +/// split). The editable payload is kept as the exact JSON shape the editor already +/// speaks (admin / new / req), so flushing to the DB can reuse +/// unchanged. +/// +public sealed class InvoiceDraftSession +{ + /// Opaque per-editor token; also the SignalR group name for targeted signals. + public string Token { get; init; } = ""; + + /// Owning user account id (drafts are single-user; used for auth + events). + public string UserAccountId { get; init; } = ""; + + /// DB invoice id once the session has been flushed (Zwischenspeichern); empty while cache-only. + public string InvId { get; set; } = ""; + + /// Always true here — sessions only ever hold unfinalised drafts. + public bool IsDraft { get; set; } = true; + + /// Bumped on every applied mutation; the browser refetches when the signalled version changes. + public int Version { get; set; } + + /// UTC of the last read/write; drives the idle sliding-TTL and expiry warnings. + public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow; + + /// Guards against sending more than one expiry warning per idle window. + public bool ExpiryWarningSent { get; set; } + + // ── Editable payload (exact editor JSON shape) ─────────────────────────── + /// Header/admin flags: type, customerid, p13b, setmode, paymentterms… + public JObject Admin { get; set; } = new(); + + /// Recipient/new fields: title/invoicetitle, invoiceaddress, invoiceemail, provisionlocation/-period, CustomValues… + public JObject New { get; set; } = new(); + + /// Service-request blocks; each block is a JObject with an items JArray (the line items). + public JArray Req { get; set; } = new(); + + // ── Computed (by the draft service; never trusted from the client) ─────── + /// Server-computed totals/VAT — the values the client used to compute in invSumUpdate. + public InvoiceDraftSums Sums { get; set; } = new(); + + /// Plausibility / consistency results, refreshed on every recompute. + public List ValidationMessages { get; } = new(); + + /// Automatic change history, appended on every applied patch. Cache-only (never persisted). + public List History { get; } = new(); + + public void Touch() => LastAccessUtc = DateTime.UtcNow; +} + +/// Server-computed invoice totals — the authoritative replacement for the browser's sms object. +public sealed class InvoiceDraftSums +{ + /// Total net (ttn). + public decimal TotalNet { get; set; } + /// Total gross (ttb); equals net when §13b reverse-charge is active. + public decimal TotalGross { get; set; } + /// Total VAT (ttvat). + public decimal TotalVat { get; set; } + /// Service net (tscn) — the service-refund base. + public decimal ServiceNet { get; set; } + /// Service VAT (tscvat). + public decimal ServiceVat { get; set; } + /// VAT amount per rate string (e.g. "19" → 123.45), matching the editor's sms.vat map. + public Dictionary VatByRate { get; } = new(); + /// Net per block, keyed by block id — feeds the per-block sub-sum row. + public Dictionary NetByBlock { get; } = new(); +} + +/// A single plausibility/consistency finding for the draft. +/// Logical field the message relates to (e.g. "email", "address", "items"). +/// "error" blocks a clean finalise; "warning"/"info" are advisory. +/// German, user-readable text. +public readonly record struct InvoiceDraftValidationMessage(string Field, string Severity, string Message); + +/// +/// One automatically-recorded change in the draft's history (shown in the +/// "Änderungshistorie" dialog). Captured on every applied patch; lives only for +/// the cache lifetime of the session and is never persisted to the database. +/// +public sealed class ChangeHistoryEntry +{ + public DateTime TimestampUtc { get; init; } = DateTime.UtcNow; + /// User account id that made the change. + public string UserAccountId { get; init; } = ""; + /// The change target/op as sent by the editor (e.g. "item.qty", "email", "p13b"). + public string Target { get; init; } = ""; + /// Optional item/block id the change applied to. + public string Ref { get; init; } = ""; + /// Previous value, stringified for display (may be empty). + public string OldValue { get; init; } = ""; + /// New value, stringified for display (may be empty). + public string NewValue { get; init; } = ""; + /// Version the session reached after applying this change. + public int Version { get; init; } +} diff --git a/Fuchs/code/InvoiceSetPricing.cs b/Fuchs/code/InvoiceSetPricing.cs index d4da348..fd3e345 100644 --- a/Fuchs/code/InvoiceSetPricing.cs +++ b/Fuchs/code/InvoiceSetPricing.cs @@ -1,4 +1,4 @@ -using static OCORE.commons; +using static OCORE.commons; using static OCORE.OCORE_dictionaries; namespace Fuchs.intranet; @@ -13,8 +13,6 @@ public enum SetDisplayMode { /// Default: show the set as one priced line; member items listed without price. SetPrice, - /// Show each member item with its own price; the set line is a header without price. - ItemPrices, /// Show only the set as one priced line; member items are removed entirely. SetOnly } @@ -22,6 +20,9 @@ public enum SetDisplayMode /// A resolved invoice display line (after applying the set display mode). public sealed class InvoiceSetLine { + /// The source item's id — lets callers (e.g. the online editor) map a resolved + /// display line back to the DOM row it should update. + public string Id { get; init; } = ""; public string Title { get; init; } = ""; public string Desc { get; init; } = ""; public string Qty { get; init; } = "1"; @@ -29,6 +30,14 @@ public sealed class InvoiceSetLine public decimal TotalNet { get; init; } /// When false, the price/total cells are rendered blank (e.g. set members in SetPrice mode). public bool ShowPrice { get; init; } = true; + /// + /// Whether this line carries a position number. Mirrors the editor's numbering + /// (InvoiceDraftCalculator.RecomputePositions): every line is numbered except + /// free-text/heading lines — independent of whether a price is shown, so a price-blanked set + /// member (SetPrice mode / block-mode nulled member) is still numbered exactly like in the + /// online editor. + /// + public bool Numbered { get; init; } = true; /// True for the set header line (rendered emphasised). public bool IsSetHeader { get; init; } } @@ -40,17 +49,24 @@ public sealed class InvoiceSetLine /// header's id. Items that belong to no set pass through unchanged. /// /// The invoice total is taken from the registration balance, not from these -/// lines, so switching modes is purely presentational and never changes the -/// invoice sum — set price always equals the sum of its members. +/// lines, so calling is a pure, non-mutating transformation +/// of the items it is given and never changes the invoice sum. A set header +/// only adopts the chosen once it has actually been +/// converted (its own total_net is non-zero, via the one-way set-item +/// switch, InvoiceDraftEditService.ApplyItemSetPrice) — until then the +/// header renders blank and each member keeps its own individual price, exactly +/// like a non-set item. (The choice of which mode to apply is itself a +/// persisted, effectively one-way decision on the draft session — see +/// Fuchs/Docs/INVOICE_SET_PRICING.md — but that persistence lives outside +/// this class.) ///
public static class InvoiceSetPricing { public static SetDisplayMode ParseMode(string? raw) => (raw ?? "").Trim().ToLowerInvariant() switch { - "itemprices" or "items" or "item" => SetDisplayMode.ItemPrices, - "setonly" or "set_only" => SetDisplayMode.SetOnly, - _ => SetDisplayMode.SetPrice + "setonly" or "set_only" => SetDisplayMode.SetOnly, + _ => SetDisplayMode.SetPrice }; /// Reads the set mode from an InvoiceOptions CSV token like "setmode:itemprices". @@ -68,9 +84,21 @@ public static class InvoiceSetPricing public static bool ContainsSets(IEnumerable> items) => items.Any(IsSetHeader) || items.Any(i => !string.IsNullOrEmpty(SetIdOf(i))); + /// + /// True if the block carries a SetItmId-linked set (a member points at a header via + /// setId) — i.e. the function-1 "set-item switch" grouping. This is the only case that + /// still routes through ; the two menu modes (ADR 0009) clear setId + /// and render flat (their inserted set row is emphasised on its own, members null out to blank). + /// + public static bool HasSetMembers(IEnumerable> items) => + items.Any(i => !string.IsNullOrEmpty(SetIdOf(i))); + /// /// Produces the ordered display lines for the given items and mode. - /// Standalone items are always shown with their price. + /// Standalone items are always shown with their price. A set header whose + /// own total_net is still zero (not yet converted via the set-item + /// switch) is rendered blank with its members individually priced, + /// regardless of . /// public static List Build(IReadOnlyList> items, SetDisplayMode mode) { @@ -87,22 +115,29 @@ public static class InvoiceSetPricing { if (IsSetHeader(item)) { - string setId = HeaderIdOf(item); - var members = membersBySet.TryGetValue(setId, out var m) ? m : new List>(); - decimal setTot = HeaderTotal(item, members); + string setId = HeaderIdOf(item); + var members = membersBySet.TryGetValue(setId, out var m) ? m : new List>(); + FuchsPdf.ParseDec(item.no("total_net", 0), out decimal headerOwnTotal); + + if (headerOwnTotal == 0) + { + // Not yet converted (the set-item switch, ApplyItemSetPrice, hasn't run): the + // header still carries its as-delivered zero price, so the chosen display mode + // does not apply yet — show the header blank and each member with its own, + // individual price, exactly as if there were no set grouping at all. + result.Add(HeaderLine(item, 0, showPrice: false)); + foreach (var mem in members) result.Add(MemberLine(mem, showPrice: true)); + continue; + } switch (mode) { case SetDisplayMode.SetPrice: - result.Add(HeaderLine(item, setTot, showPrice: true)); + result.Add(HeaderLine(item, headerOwnTotal, showPrice: true)); foreach (var mem in members) result.Add(MemberLine(mem, showPrice: false)); break; - case SetDisplayMode.ItemPrices: - result.Add(HeaderLine(item, setTot, showPrice: false)); // grouping title, no price (avoid double count) - foreach (var mem in members) result.Add(MemberLine(mem, showPrice: true)); - break; case SetDisplayMode.SetOnly: - result.Add(HeaderLine(item, setTot, showPrice: true)); + result.Add(HeaderLine(item, headerOwnTotal, showPrice: true)); break; } } @@ -145,23 +180,16 @@ public static class InvoiceSetPricing return string.IsNullOrEmpty(s) ? i.nz("id", "") : s; } - private static decimal HeaderTotal(Dictionary header, List> members) - { - FuchsPdf.ParseDec(header.no("total_net", 0), out decimal headerTot); - if (headerTot != 0) return headerTot; - decimal sum = 0; - foreach (var m in members) { FuchsPdf.ParseDec(m.no("total_net", 0), out decimal t); sum += t; } - return sum; - } - private static InvoiceSetLine HeaderLine(Dictionary i, decimal setTotal, bool showPrice) => new() { + Id = i.nz("id", ""), Title = i.nz("title", ""), Desc = i.nz("desc", ""), Qty = i.nz("qty", "1"), PriceNet = setTotal, TotalNet = setTotal, ShowPrice = showPrice, + Numbered = true, // a set header is always numbered IsSetHeader = true }; @@ -171,12 +199,14 @@ public static class InvoiceSetPricing FuchsPdf.ParseDec(i.no("total_net", 0), out decimal total); return new InvoiceSetLine { + Id = i.nz("id", ""), Title = i.nz("title", ""), Desc = i.nz("desc", ""), Qty = i.nz("qty", "1"), PriceNet = price, TotalNet = total, ShowPrice = showPrice && !IsNoPriceLine(i), // headings/free text print no price + Numbered = !IsNoPriceLine(i), // …but are still numbered unless heading/free-text IsSetHeader = false }; } diff --git a/Fuchs/code/ReminderDraftCalculator.cs b/Fuchs/code/ReminderDraftCalculator.cs new file mode 100644 index 0000000..899d888 --- /dev/null +++ b/Fuchs/code/ReminderDraftCalculator.cs @@ -0,0 +1,92 @@ +using System.Globalization; +using Newtonsoft.Json.Linq; + +namespace Fuchs.intranet; + +/// +/// Server-side, pure aggregation of a reminder draft's open amount — the authoritative +/// replacement for the browser's inline figure (ADR 0006, mirroring +/// ). The user's requirement is that the computed +/// figure lives in the backend cache, not the frontend. +/// +/// A reminder chases a single invoiced amount: AmountOpen = AmountTotal - AmountPayed +/// (both read from the editor's new block). Static/pure, hence exhaustively +/// unit-testable. +/// +public static class ReminderDraftCalculator +{ + /// Recomputes the reminder's open amount from the edited amount / amount_payed. + public static void RecomputeTotals(ReminderDraftSession session) + { + decimal total = Dec(session.New["amount"]); + decimal payed = Dec(session.New["amount_payed"]); + session.Sums = new ReminderDraftSums + { + AmountTotal = total, + AmountPayed = payed, + AmountOpen = total - payed + }; + } + + /// + /// Refreshes the draft's plausibility / consistency findings. "error" severity marks + /// issues that should block a clean finalise; "warning" is advisory. Kept in German, + /// user-readable, so the frontend can render them directly. + /// + public static void Validate(ReminderDraftSession session) + { + session.ValidationMessages.Clear(); + void Add(string field, string sev, string msg) => + session.ValidationMessages.Add(new ReminderDraftValidationMessage(field, sev, msg)); + + string email = Str(session.New["invoiceemail"]).Trim(); + if (email.Length == 0) + Add("email", "warning", "Es ist keine E-Mail-Adresse hinterlegt — die Mahnung kann nicht per E-Mail versandt werden."); + else if (!IsValidEmail(email)) + Add("email", "error", "Die E-Mail-Adresse ist ungültig."); + + if (Str(session.New["invoiceaddress"]).Trim().Length == 0) + Add("address", "warning", "Es ist keine Anschrift hinterlegt."); + + if (Str(session.New["subject"]).Trim().Length == 0) + Add("subject", "warning", "Es ist kein Betreff hinterlegt."); + + if (session.Sums.AmountOpen <= 0) + Add("amount", "warning", "Der offene Betrag ist null oder negativ — es besteht keine offene Forderung."); + } + + // ── helpers ────────────────────────────────────────────────────────────── + /// Parses a JToken to a decimal, tolerating German ("12,50" / "1.234,56") and invariant ("12.50") strings. + internal static decimal Dec(JToken? token) + { + if (token == null || token.Type == JTokenType.Null) return 0; + if (token.Type is JTokenType.Float or JTokenType.Integer) return token.Value(); + return ParseAmount(Str(token)); + } + + /// + /// Parses a currency string, resolving the German/invariant ambiguity: a value with both + /// separators treats "." as thousands and "," as decimal ("1.234,56"); a value with only "," + /// treats it as the decimal separator ("12,50"); otherwise it is parsed invariant ("1234.56"). + /// + internal static decimal ParseAmount(string? raw) + { + string s = (raw ?? "").Trim(); + if (s.Length == 0) return 0; + bool hasComma = s.Contains(','), hasDot = s.Contains('.'); + if (hasComma && hasDot) s = s.Replace(".", "").Replace(',', '.'); // German "1.234,56" + else if (hasComma) s = s.Replace(',', '.'); // German "12,50" + return decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out decimal d) ? d : 0; + } + + private static string Str(JToken? token) => + token == null || token.Type == JTokenType.Null ? "" : token.Type == JTokenType.String ? token.Value() ?? "" : token.ToString(); + + private static bool IsValidEmail(string email) + { + int at = email.IndexOf('@'); + if (at <= 0 || at != email.LastIndexOf('@')) return false; + int dot = email.IndexOf('.', at); + return dot > at + 1 && dot < email.Length - 1; + } +} diff --git a/Fuchs/code/ReminderDraftSession.cs b/Fuchs/code/ReminderDraftSession.cs new file mode 100644 index 0000000..2654042 --- /dev/null +++ b/Fuchs/code/ReminderDraftSession.cs @@ -0,0 +1,81 @@ +using Newtonsoft.Json.Linq; + +namespace Fuchs.intranet; + +/// +/// Server-side, in-memory editing state for a single reminder (Zahlungserinnerung) +/// draft — the authoritative source of truth while a back-office user edits a draft in +/// the browser. This mirrors for reminders (ADR 0006): +/// the browser is a pure view/input layer that posts single changes +/// (); the server mutates this session, +/// recomputes the open amount and validates, then signals the browser to re-fetch. +/// +/// This is a data holder only — all calculation, validation, persistence and +/// rendering live in (mirroring the +/// / split). +/// The editable payload is kept as the exact JSON shape the editor already speaks +/// (new / rem), so flushing to the DB can reuse +/// unchanged. +/// The change-history record type () is shared with the +/// invoice draft; validation messages use the reminder-specific +/// . +/// +public sealed class ReminderDraftSession +{ + /// Opaque per-editor token; also the SignalR group name for targeted signals. + public string Token { get; init; } = ""; + + /// Owning user account id (drafts are single-user; used for auth + events). + public string UserAccountId { get; init; } = ""; + + /// DB reminder id once the session has been flushed (Zwischenspeichern); empty while cache-only. + public string RemId { get; set; } = ""; + + /// Always true here — sessions only ever hold unfinalised drafts. + public bool IsDraft { get; set; } = true; + + /// Bumped on every applied mutation; the browser refetches when the signalled version changes. + public int Version { get; set; } + + /// UTC of the last read/write; drives the idle sliding-TTL and expiry warnings. + public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow; + + /// Guards against sending more than one expiry warning per idle window. + public bool ExpiryWarningSent { get; set; } + + // ── Editable payload (exact editor JSON shape) ─────────────────────────── + /// Recipient/new fields: subject, invoiceaddress, invoiceemail, text, amount, amount_payed, CustomValues… + public JObject New { get; set; } = new(); + + /// Reference fields: invid, type, level, invoiceid, invoicedate, sender… + public JObject Rem { get; set; } = new(); + + // ── Computed (by the draft service; never trusted from the client) ─────── + /// Server-computed open-amount aggregation — the values the client used to compute inline. + public ReminderDraftSums Sums { get; set; } = new(); + + /// Plausibility / consistency results, refreshed on every recompute. + public List ValidationMessages { get; } = new(); + + /// Automatic change history, appended on every applied patch. Cache-only (never persisted). + public List History { get; } = new(); + + public void Touch() => LastAccessUtc = DateTime.UtcNow; +} + +/// Server-computed reminder totals — the authoritative open-amount for the draft. +public sealed class ReminderDraftSums +{ + /// Invoiced amount (gross) the reminder chases. + public decimal AmountTotal { get; set; } + /// Amount already paid against the invoice. + public decimal AmountPayed { get; set; } + /// Still-open amount (AmountTotal - AmountPayed) — the reminder's headline figure. + public decimal AmountOpen { get; set; } +} + +/// A single plausibility/consistency finding for the reminder draft. +/// Logical field the message relates to (e.g. "email", "address", "amount"). +/// "error" blocks a clean finalise; "warning"/"info" are advisory. +/// German, user-readable text. +public readonly record struct ReminderDraftValidationMessage(string Field, string Severity, string Message); diff --git a/Fuchs/dotnet-tools.json b/Fuchs/dotnet-tools.json new file mode 100644 index 0000000..cce89d8 --- /dev/null +++ b/Fuchs/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "10.0.10", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/Fuchs/js/intranet/fis_main.js b/Fuchs/js/intranet/fis_main.js index c1d065a..02f9b77 100644 --- a/Fuchs/js/intranet/fis_main.js +++ b/Fuchs/js/intranet/fis_main.js @@ -284,3 +284,65 @@ $fis.notifications = { }, 9000); } }; + +/* Live draft-editing client (ADR 0006/0007). Separate SignalR connection to the + dedicated /draftpreview hub; the server signals the *one* browser editing a draft + (group = session token) to re-fetch (draftReady), warns before idle expiry + (draftExpiring), and tells it to close on eviction (draftClosed). The editor + (fis.inv_shared.js) registers the open draft via $fis.draft.bind(token, {...}). */ +$fis.draft = { + connection: null, + active: null, /* { token, onReady(version), onExpiring(secondsLeft), onClosed(reason) } */ + init: function () { + if (typeof signalR === 'undefined' || this.connection !== null || !$ocms.auth.useraccount_id) { + return; + } + this.connection = new signalR.HubConnectionBuilder() + .withUrl('/draftpreview') + .withAutomaticReconnect() + .build(); + this.connection.on('draftReady', (p) => this._dispatch('onReady', p, (p) => p.version)); + this.connection.on('draftExpiring', (p) => this._dispatch('onExpiring', p, (p) => p.secondsLeft)); + this.connection.on('draftClosed', (p) => this._dispatch('onClosed', p, (p) => p.reason)); + /* Re-join the active draft's group after a (re)connect — group membership is + per-connection and is lost when the socket drops. */ + this.connection.onreconnected(() => { if (this.active) { this._invoke('JoinDraft', this.active.token); } }); + this.connection.onclose(() => { + console.warn('Draft connection closed; retrying in 5s.'); + this.connection = null; + setTimeout(() => { this.init(); if (this.active) { this.bind(this.active.token, this.active); } }, 5000); + }); + this.start(); + }, + start: function () { + this.connection.start() + .then(() => { if (this.active) { this._invoke('JoinDraft', this.active.token); } }) + .catch((err) => { + console.warn('Draft connection failed to start; retrying in 5s.', err); + this.connection = null; + setTimeout(() => this.init(), 5000); + }); + }, + /* Registers the currently open draft and joins its signal group. handlers: + { onReady, onExpiring, onClosed }. */ + bind: function (token, handlers) { + if (!token) { return; } + this.active = $.extend({ token: token }, handlers || {}); + if (this.connection === null) { this.init(); } + this._invoke('JoinDraft', token); + }, + /* Unregisters + leaves the group (editor closed). */ + release: function (token) { + if (this.active && (!token || this.active.token === token)) { this.active = null; } + this._invoke('LeaveDraft', token); + }, + _invoke: function (method, token) { + if (!token || !this.connection || this.connection.state !== 'Connected') { return; } + this.connection.invoke(method, token).catch((err) => console.warn('Draft ' + method + ' failed', err)); + }, + _dispatch: function (handler, payload, argOf) { + payload = payload || {}; + if (!this.active || this.active.token !== payload.token) { return; } + if (typeof this.active[handler] === 'function') { this.active[handler](argOf(payload)); } + } +}; diff --git a/Fuchs/js/intranet/fis_main_go.js b/Fuchs/js/intranet/fis_main_go.js index 8dec451..24a03b1 100644 --- a/Fuchs/js/intranet/fis_main_go.js +++ b/Fuchs/js/intranet/fis_main_go.js @@ -1,4 +1,6 @@ $(document).ready(function () { $fis.notifications.init(); + $fis.draft.init(); $fis.ov(); + $fis.addAdminMenuIfAuthorized(); }); diff --git a/Fuchs/js/intranet/fis_main_menu.js b/Fuchs/js/intranet/fis_main_menu.js index e5da403..3be1d2e 100644 --- a/Fuchs/js/intranet/fis_main_menu.js +++ b/Fuchs/js/intranet/fis_main_menu.js @@ -9,4 +9,21 @@ , { lbl: $t.m_todo, id: 'm_todo', fnc: () => { $('#contentframe').empty().load($ocms.url('todos')); $('#listframe').rC('fix').aC('hd'); }, ico: 'glyphicon glyphicon-sunglasses' } //, { lbl: $t.m_efa, id: 'm_efa', fnc: 'init:efa' } ]); -})(); \ No newline at end of file +})(); + +/* The Administration module is restricted to users whose fds_sys authorization exceeds 4. + The button is added — and only then does clicking it load the module script — after the + asynchronous auth check resolves, so lower-privileged users never see it or fetch the code. + Called from fis_main_go.js once the DOM (and the initial main menu) is ready. */ +$fis.addAdminMenuIfAuthorized = function () { + if ($('#m_admin').length > 0) { return; } // idempotent + $fis.getAuth('fds_sys').then(function (auth) { + if ((auth || -1) > 4 && $('#m_admin').length === 0) { + $ocms.ocmsmenu.push({ lbl: $t.m_admin, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-cog' }); + /* Re-render the main menu cleanly: drop the previously generated menu list + (never the .nav-right settings dropdown) and rebuild it from the array. */ + $('#mainmenu').children('ul:not(.nav-right)').remove(); + $('#mainmenu').ocmsmenu($ocms.ocmsmenu); + } + }).catch(function () { /* auth lookup failed → leave the menu unchanged */ }); +}; \ No newline at end of file diff --git a/Fuchs/js/intranet/fis_texts_gui_de.js b/Fuchs/js/intranet/fis_texts_gui_de.js index ef6ce50..a1f5336 100644 --- a/Fuchs/js/intranet/fis_texts_gui_de.js +++ b/Fuchs/js/intranet/fis_texts_gui_de.js @@ -4,6 +4,7 @@ , m_rep: 'Berichte' , m_todo: 'ToDos' , m_bcd: 'BankBuchungen' + , m_admin: 'Administration' , rsp: 'Passwort ändern' , pnm: 'Die Passwörter stimmen nicht überein' , cps: 'Das neue Passwort wurde gespeichert.' diff --git a/Fuchs/js/intranet/modules/fis.admin.js b/Fuchs/js/intranet/modules/fis.admin.js new file mode 100644 index 0000000..5690598 --- /dev/null +++ b/Fuchs/js/intranet/modules/fis.admin.js @@ -0,0 +1,245 @@ +let $adm = { + init2: function () { + // No external libraries needed — go straight to render. + $ocms.getScript([], function () { $adm.init3(); }); + }, + init3: function () { + $fis.cf(true); + $fis.lf(true); + $('#activemodule').text($adt.mdl); + $adm.topbar(); + let frm = $$.dc('admfrm', $('#contentframe')).ldng(1); + $adm.load(frm); + }, + topbar: function () { + return $('#topbar').ocmsmenu([ + { lbl: $adt.refreshAll, id: 'adm_refresh_all', ico: 'glyphicon glyphicon-refresh', fnc: function () { $adm.init3(); } } + ]); + }, + load: function (frm) { + $ocms.postXT({ + url: $ocms.url('admin/status'), success: function (r) { + frm.empty(); + $adm.render(frm, r.info || {}, r.probes || []); + }, error: function () { + frm.empty(); + $$.dc('adm_err', frm).text($adt.loadfail); + }, complete: function () { frm.ldng(0); } + }); + }, + render: function (frm, info, probes) { + let pmap = {}; + $.each(probes, function (i, p) { pmap[p.component] = p; }); + + $$.dc('adm_sec', frm).text($adt.sub_info); + $adm.sysCard(frm, info); + $adm.startupCard(frm, info.startupChecks); + + $$.dc('adm_sec', frm).text($adt.sub_status); + let grid = $$.dc('adm_grid', frm); + $adm.dbCard(grid, info.database || {}, pmap.database); + $adm.mailCard(grid, info.email || {}); + $adm.blobCard(grid, info.blob || {}, pmap.blob); + $adm.mfrCard(grid, info.mfr || {}, pmap.mfr); + $adm.kvCard(grid, info.keyVault || {}, pmap.keyvault); + }, + + /* ── generic building blocks ─────────────────────────────────────────── */ + kvTable: function (parent, rows) { + let t = $$.tbl().addClass('adm_kv'); + $.each(rows || [], function (i, r) { + if (!r) { return true; } + let tr = $$.tr(t); + $$.td(tr).addClass('k').text(r[0] == null ? '' : String(r[0])); + let v = $$.td(tr).addClass('v'); + if (r[1] instanceof jQuery) { v.append(r[1]); } else { v.text(r[1] == null ? '' : String(r[1])); } + }); + if (parent instanceof jQuery) { parent.append(t); } + return t; + }, + yesNo: function (b) { + return $$.sc('adm_bool adm_bool_' + (b ? 'y' : 'n'), b ? $adt.yes : $adt.no); + }, + setPill: function (pill, probe) { + probe = probe || {}; + let st = probe.status || 'unconfigured'; + let lbl = { + ok: $adt.st_ok, error: $adt.st_error, disabled: $adt.st_disabled, + unconfigured: $adt.st_unconfigured, checking: $adt.st_checking + }[st] || st; + pill.attr('class', 'adm_pill adm_pill_' + st).text(lbl); + return pill; + }, + + /* Connectivity card with a live status pill + per-card refresh button. */ + probeCard: function (grid, title, component, cfgRows, probe) { + let card = $$.dc('adm_card', grid); + let hd = $$.dc('adm_card_hd', card); + $$.s(title).appendTo(hd); + let pill = $$.sc('adm_pill').appendTo(hd); + let rbtn = $$.bbtn($adt.refresh, 'adm_btn').appendTo(hd); + let bd = $$.dc('adm_card_bd', card); + $adm.kvTable(bd, cfgRows); + + let pf = $$.dc('adm_probe', bd); + let msg = $$.dc('adm_probe_msg', pf); + let met = $$.dc('adm_probe_metrics', pf); + let det = $$.dc('adm_probe_det', pf); + let dur = $$.dc('adm_probe_dur', pf); + let apply = function (p) { + p = p || {}; + $adm.setPill(pill, p); + msg.text(p.message || ''); + met.empty(); + $.each(p.metrics || [], function (i, m) { + let row = $$.dc('adm_metric', met); + $$.sc('adm_metric_k').text(m.label || '').appendTo(row); + $$.sc('adm_metric_v').text(m.value == null ? '' : String(m.value)).appendTo(row); + }); + det.text(p.detail || ''); + dur.text(p.durationMs != null ? (p.durationMs + ' ms') : ''); + }; + apply(probe); + + rbtn.click(function () { + $adm.setPill(pill, { status: 'checking' }); + msg.text(''); met.empty(); det.text(''); dur.text(''); + $ocms.postXT({ + url: $ocms.url('admin/probe/' + component), success: function (r) { + apply(r.probe); + }, error: function () { + $adm.setPill(pill, { status: 'error' }); + msg.text($adt.st_error); + } + }); + }); + return card; + }, + + /* ── individual cards ────────────────────────────────────────────────── */ + sysCard: function (frm, info) { + let card = $$.dc('adm_card adm_card_wide', frm); + let hd = $$.dc('adm_card_hd', card); + $$.s($adt.host).appendTo(hd); + let env = $$.sc('adm_env').text(info.environment || ''); + if (info.isTestDeployment === true) { env.aC('adm_env_test'); } + env.appendTo(hd); + let bd = $$.dc('adm_card_bd', card); + $adm.kvTable(bd, [ + [$adt.machine, info.machineName], + [$adt.os, info.osDescription], + [$adt.framework, info.frameworkDescription], + [$adt.env, info.environment], + [$adt.testdep, $adm.yesNo(info.isTestDeployment === true)], + [$adt.pid, info.processId], + [$adt.since, info.processStartUtc ? fdt(info.processStartUtc) : ''], + [$adt.uptime, (info.uptimeHours != null ? (info.uptimeHours + ' ' + $adt.hours) : '')] + ]); + }, + /* Non-refreshable widget: the one-shot startup self-test result captured at boot. */ + startupCard: function (frm, sc) { + let card = $$.dc('adm_card adm_card_wide', frm); + let hd = $$.dc('adm_card_hd', card); + $$.s($adt.startup).appendTo(hd); + let bd = $$.dc('adm_card_bd', card); + + if (!sc || sc.ran !== true) { + $adm.setPill($$.sc('adm_pill').appendTo(hd), { status: 'disabled' }); + $$.dc('adm_note', bd).text($adt.startup_notrun); + return; + } + + let items = sc.items || []; + let anyFail = false; + $.each(items, function (i, it) { if (it.enabled === true && it.ok !== true) { anyFail = true; } }); + $adm.setPill($$.sc('adm_pill').appendTo(hd), { status: anyFail ? 'error' : 'ok' }); + + $adm.kvTable(bd, [ + [$adt.startup_on, sc.machineName], + [$adt.startup_at, sc.completedUtc ? fdt(sc.completedUtc) : ''] + ]); + + let list = $$.dc('adm_checks', bd); + $.each(items, function (i, it) { + let row = $$.dc('adm_check', list); + $$.sc('adm_check_k').text($adt['chk_' + it.name] || it.name).appendTo(row); + let stt = it.enabled !== true ? 'skipped' : (it.ok === true ? 'ok' : 'fail'); + let lbl = { ok: $adt.chk_ok, fail: $adt.chk_fail, skipped: $adt.chk_skipped }[stt]; + $$.sc('adm_check_v adm_check_' + stt).text(lbl).appendTo(row); + }); + }, + dbCard: function (grid, d, probe) { + $adm.probeCard(grid, $adt.db, 'database', [ + [$adt.configured, $adm.yesNo(d.configured === true)], + [$adt.server, d.server], + [$adt.catalog, d.catalog], + [$adt.login, d.userId] + ], probe); + }, + mailCard: function (grid, e) { + let card = $$.dc('adm_card', grid); + let hd = $$.dc('adm_card_hd', card); + $$.s($adt.mail).appendTo(hd); + $adm.setPill($$.sc('adm_pill').appendTo(hd), { status: e.mailerEnabled === true ? 'ok' : 'disabled' }); + let bd = $$.dc('adm_card_bd', card); + let rows = [ + [$adt.mailer, $adm.yesNo(e.mailerEnabled === true)], + [$adt.baseurl, e.baseUrl], + [$adt.account, e.accountId], + [$adt.serverid, e.serverId], + [$adt.token, $adm.yesNo(e.tokenConfigured === true)], + [$adt.overrideActive, $adm.yesNo(e.overrideActive === true)] + ]; + if (e.overrideActive === true) { rows.push([$adt.override, e.overrideRecipient]); } + $adm.kvTable(bd, rows); + let ft = $$.dc('adm_card_ft', card); + $$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg); + }, + blobCard: function (grid, b, probe) { + $adm.probeCard(grid, $adt.blob, 'blob', [ + [$adt.enabled, $adm.yesNo(b.enabled === true)], + [$adt.configured, $adm.yesNo(b.configured === true)], + [$adt.invContainer, b.invoiceContainer], + [$adt.remContainer, b.reminderContainer] + ], probe); + }, + mfrCard: function (grid, m, probe) { + $adm.probeCard(grid, $adt.mfr, 'mfr', [ + [$adt.host_l, m.host], + [$adt.creds, $adm.yesNo(m.credentialsConfigured === true)], + [$adt.sync, $adm.yesNo(m.syncEnabled === true)] + ], probe); + }, + kvCard: function (grid, k, probe) { + $adm.probeCard(grid, $adt.kv, 'keyvault', [ + [$adt.vault, k.vaultUri], + [$adt.appname, k.appName], + [$adt.seccount, k.managedSecretCount], + [$adt.clientreg, $adm.yesNo(k.clientRegistered === true)] + ], probe); + }, + + /* ── test email ──────────────────────────────────────────────────────── */ + testMailDlg: function () { + $ocms.dlgform([ + { name: 'to', label: $adt.tm_to, type: 'email', required: true, value: ($ocms.auth.email || '') }, + { name: 'subject', label: $adt.tm_subject, type: 'string', required: true, value: $adt.tm_default_subject }, + { name: 'body', label: $adt.tm_body, type: 'text', required: true, value: $adt.tm_default_body } + ], { + title: $adt.testmail, + button: $adt.testmail_btn, + size: [420, 560], + addcontent: $$.dc('adm_note').text($adt.tm_hint), + url: $ocms.url('admin/testmail'), + typedvalues: true, + /* Runs inside jQuery's success dispatch — it must never throw, or postXT's + `complete` (which clears the page loading indicator) is skipped. */ + success: function (r) { + let res = (r && r.result) || {}; + alert(res.message || (res.sent === true ? $adt.tm_sent : $adt.tm_failed)); + } + }); + } +}; +let $$adm = { init2: $adm.init2, auth: {} }; +export default $$adm; diff --git a/Fuchs/js/intranet/modules/fis.admin.scss b/Fuchs/js/intranet/modules/fis.admin.scss new file mode 100644 index 0000000..2616245 --- /dev/null +++ b/Fuchs/js/intranet/modules/fis.admin.scss @@ -0,0 +1,200 @@ +.admfrm { + padding: 1.5rem; + + .adm_sec { + font-size: 1.2rem; + font-weight: bold; + color: $fuchs_blau; + margin: 1.5rem 0 0.75rem; + border-bottom: 2px solid $fuchs_blau; + padding-bottom: 0.25rem; + + &:first-child { margin-top: 0; } + } + + .adm_err { + padding: 1rem; + color: #b00020; + font-weight: bold; + } + + .adm_grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr)); + gap: 1rem; + align-items: start; + } + + .adm_card { + border: 1px solid #d5d5d5; + border-radius: 0.4rem; + background: #fff; + box-shadow: 0 1px 3px rgba(50, 50, 50, 0.12); + overflow: hidden; + + &.adm_card_wide { grid-column: 1 / -1; } + } + + .adm_card_hd { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 0.85rem; + background: $fuchs_blau; + color: #fff; + font-weight: bold; + + > span:first-child { flex: 1 1 auto; } + } + + .adm_card_bd { + padding: 0.6rem 0.85rem; + } + + .adm_card_ft { + padding: 0.5rem 0.85rem 0.75rem; + text-align: right; + } + + table.adm_kv { + border-collapse: collapse; + width: 100%; + + td { + padding: 0.25rem 0.35rem; + border-bottom: 1px solid #eee; + vertical-align: top; + font-size: 0.92rem; + + &.k { color: #666; white-space: nowrap; width: 40%; } + &.v { word-break: break-word; } + } + + tr:last-child td { border-bottom: none; } + } + + .adm_probe { + margin-top: 0.6rem; + padding-top: 0.5rem; + border-top: 1px dashed #ddd; + font-size: 0.88rem; + + .adm_probe_msg { font-weight: 600; } + .adm_probe_det { color: #555; word-break: break-word; margin-top: 0.15rem; } + .adm_probe_dur { color: #999; margin-top: 0.15rem; font-size: 0.8rem; } + + .adm_probe_metrics { + margin-top: 0.35rem; + + .adm_metric { + display: flex; + justify-content: space-between; + gap: 0.5rem; + padding: 0.1rem 0; + border-bottom: 1px dotted #eee; + + &:last-child { border-bottom: none; } + + .adm_metric_k { color: #555; word-break: break-word; } + .adm_metric_v { font-weight: 600; white-space: nowrap; } + } + } + } + + /* startup checks list */ + .adm_checks { + margin-top: 0.5rem; + + .adm_check { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0; + border-bottom: 1px solid #eee; + + &:last-child { border-bottom: none; } + + .adm_check_k { color: #333; } + .adm_check_v { + font-weight: bold; + font-size: 0.8rem; + padding: 0.05rem 0.5rem; + border-radius: 0.25rem; + white-space: nowrap; + } + .adm_check_ok { background: #e2f3da; color: #2f6d18; } + .adm_check_fail { background: #f3ded9; color: #c0341d; } + .adm_check_skipped { background: #ececec; color: #777; } + } + } + + /* status pill */ + .adm_pill { + display: inline-block; + padding: 0.1rem 0.55rem; + border-radius: 1rem; + font-size: 0.78rem; + font-weight: bold; + color: #fff; + background: #999; + white-space: nowrap; + } + .adm_pill_ok { background: $fuchs_akzent; } + .adm_pill_error { background: #c0341d; } + .adm_pill_disabled { background: #888; } + .adm_pill_unconfigured { background: #d69100; } + .adm_pill_checking { background: #2a72c4; } + + /* boolean chips */ + .adm_bool { + display: inline-block; + padding: 0 0.4rem; + border-radius: 0.25rem; + font-size: 0.8rem; + font-weight: bold; + } + .adm_bool_y { background: #e2f3da; color: #2f6d18; } + .adm_bool_n { background: #f3e0dd; color: #9c2a17; } + + .adm_env { + font-size: 0.8rem; + font-weight: normal; + padding: 0.1rem 0.5rem; + border-radius: 0.25rem; + background: rgba(255, 255, 255, 0.25); + + &.adm_env_test { background: #13a143; } + } + + .adm_btn { + cursor: pointer; + border-radius: 0.28rem; + padding: 0.2rem 0.6rem; + font-size: 0.85rem; + border: 1px solid #ababab; + background-color: #fff; + color: #333; + + &.adm_btn_primary { + background-color: $fuchs_akzent; + border-color: darken($fuchs_akzent, 8%); + color: #fff; + } + } + + .adm_card_hd .adm_btn { + background-color: rgba(255, 255, 255, 0.15); + color: #fff; + border-color: rgba(255, 255, 255, 0.4); + } +} + +.adm_note { + margin-top: 0.75rem; + padding: 0.5rem 0.6rem; + background: #f3f6fb; + border-left: 3px solid $fuchs_blau; + font-size: 0.85rem; + color: #444; +} diff --git a/Fuchs/js/intranet/modules/fis.admin_txt_de.js b/Fuchs/js/intranet/modules/fis.admin_txt_de.js new file mode 100644 index 0000000..7a8fcf3 --- /dev/null +++ b/Fuchs/js/intranet/modules/fis.admin_txt_de.js @@ -0,0 +1,84 @@ +let $adt = { + mdl: 'Administration' + , sub_info: 'System' + , sub_status: 'Status & Konnektivität' + , sub_tests: 'Tests' + // startup checks widget (non-refreshable) + , startup: 'Startup-Prüfungen' + , startup_notrun: 'Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).' + , startup_on: 'Host' + , startup_at: 'Ausgeführt (UTC)' + , chk_ok: 'OK' + , chk_fail: 'Fehler' + , chk_skipped: 'übersprungen' + , chk_KeyVault: 'Azure Key Vault' + , chk_Database: 'SQL-Datenbank' + , chk_MFR: 'MFR (ERP)' + , chk_PdfLicense: 'Spire.PDF-Lizenz' + , chk_Mailer: 'E-Mail (Startup-Probe)' + // system info + , host: 'Host / Server' + , machine: 'Hostname' + , os: 'Betriebssystem' + , framework: 'Framework' + , env: 'Umgebung' + , testdep: 'Test-Deployment' + , pid: 'Prozess-ID' + , uptime: 'Laufzeit' + , since: 'Start (UTC)' + , hours: 'Std.' + // status cards + , db: 'SQL-Datenbank' + , mail: 'E-Mail-Dienst' + , blob: 'Azure Blob Storage' + , mfr: 'MFR (ERP)' + , kv: 'Azure Key Vault' + // labels + , server: 'Server' + , catalog: 'Datenbank' + , login: 'Login' + , mailer: 'Mailer aktiv' + , baseurl: 'Base-URL' + , account: 'Account' + , serverid: 'Server-ID' + , token: 'Token hinterlegt' + , override: 'OverrideRecipient' + , overrideActive: 'Umleitung aktiv' + , enabled: 'Aktiviert' + , configured: 'Konfiguriert' + , invContainer: 'Rechnungs-Container' + , remContainer: 'Mahnungs-Container' + , host_l: 'Host' + , creds: 'Zugangsdaten hinterlegt' + , sync: 'Sync aktiv' + , vault: 'Vault-URI' + , appname: 'App-Präfix' + , seccount: 'Verwaltete Secrets' + , clientreg: 'Client registriert' + , duration: 'Dauer' + , checked: 'Geprüft' + , detail: 'Detail' + // status values + , st_ok: 'OK' + , st_error: 'Fehler' + , st_disabled: 'Deaktiviert' + , st_unconfigured: 'Nicht konfiguriert' + , st_checking: 'Prüfe…' + // actions + , refresh: 'Aktualisieren' + , refreshAll: 'Alle prüfen' + , yes: 'Ja' + , no: 'Nein' + // test email + , testmail: 'Test-E-Mail' + , testmail_btn: 'Test-E-Mail senden' + , tm_to: 'Empfänger' + , tm_subject: 'Betreff' + , tm_body: 'Nachricht' + , tm_hint: 'Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.' + , tm_sent: 'Test-E-Mail übergeben.' + , tm_failed: 'Test-E-Mail konnte nicht gesendet werden.' + , tm_default_subject: 'Fuchs Intranet – Test-E-Mail' + , tm_default_body: 'Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.' + , loadfail: 'Der Systemstatus konnte nicht geladen werden.' +}; diff --git a/Fuchs/js/intranet/modules/fis.bam.js b/Fuchs/js/intranet/modules/fis.bam.js index 486ba7e..8a8dc9a 100644 --- a/Fuchs/js/intranet/modules/fis.bam.js +++ b/Fuchs/js/intranet/modules/fis.bam.js @@ -36,11 +36,16 @@ let $bam = { let fds = bool(ix.fds, false), id = ix.taID, m = []; m.push({ lbl: $bct.smd, fnc: () => { $bam.smd(id); } }); m.push({ lbl: $bct.ati, fnc: () => { $bam.ati(id); } }); + if (bool(ix.IsManual, false) === true) { + m.push({ lbl: $bct.mbc, fnc: () => { $bam.mbc(id); } }); + m.push({ lbl: $bct.mbd, fnc: () => { $bam.mbd(id); } }); + } return $('#topbar').ocmsmenu(m); }, eM : (r, re, opt) => { let m = []; if ($fis.isAuth('fds_bam', 2) === true) { m.push({ lbl: $bct.upl, fnc: $bam.ubs}); + m.push({ lbl: $bct.mbe, fnc: $bam.mbe}); } if (bool(r, false) === true) { m.push({ lbl: $bct.rel, fnc: $bam.renderLst }); @@ -312,6 +317,32 @@ let $bam = { } } }); + }, mbe: () => { + $bam.manForm({}); + }, mbc: (taid) => { + $ocms.postXT({ + url: $ocms.url('bam/mget'), data: { taid: taid }, success: (dta) => { + $bam.manForm(dta || {}); + } + }); + }, mbd: (taid) => { + if (confirm($bct.mbdc)) { + $ocms.postXT({ + url: $ocms.url('bam/mdel'), data: { taid: taid }, success: () => { + $bam.renderLst(); + } + }); + } + }, manForm: (dta) => { + let flds = $bcol.man.clone().applyValues(dta || {}); + $ocms.dlgform(flds, { + title: (ne(dta.taID, '') !== '') ? $bct.mbc : $bct.mbe, + url: $ocms.url('bam/man'), + typedvalues: true, + success: () => { + $bam.renderLst(); + } + }); } } diff --git a/Fuchs/js/intranet/modules/fis.bam_txt_de.js b/Fuchs/js/intranet/modules/fis.bam_txt_de.js index b42e792..86a6808 100644 --- a/Fuchs/js/intranet/modules/fis.bam_txt_de.js +++ b/Fuchs/js/intranet/modules/fis.bam_txt_de.js @@ -18,10 +18,21 @@ }, in: 'Rechnungsnummer', bt: 'Buchungstexte', - bv: 'Buchungswert (volle €)' + bv: 'Buchungswert (volle €)', + mbe: 'Manuelle Buchung erfassen', + mbc: 'Manuelle Buchung ändern', + mbd: 'Manuelle Buchung löschen', + mbdc: 'Manuelle Buchung wirklich löschen?\nDieser Vorgang kann nicht rückgängig gemacht werden.' }; let $bcol = { + man: new fields_definition('Manuelle Buchung', 'Manuelle Buchungen', [ + { name: 'taID', type: 'hidden' }, + { name: 'ValueDate', label: 'Valuta', type: 'date', required: true }, + { name: 'Amount', label: 'Betrag', type: 'number', precision: '0.01', required: true }, + { name: 'NameOfPayer', label: 'Name', type: 'string' }, + { name: 'SepaRemittanceInformation', label: 'Verwendungszweck', type: 'string' } + ]), qtl: new fields_definition('Kontobericht', 'Kontoberichte', [ { name: 'InvoiceId', label: 'RechnungsNr', type: 'string' }, { name: 'ValueDate', label: 'Valuta', type: 'date' }, diff --git a/Fuchs/js/intranet/modules/fis.inv_shared.js b/Fuchs/js/intranet/modules/fis.inv_shared.js index 96b5cd1..5b1e744 100644 --- a/Fuchs/js/intranet/modules/fis.inv_shared.js +++ b/Fuchs/js/intranet/modules/fis.inv_shared.js @@ -82,6 +82,11 @@ $inv.iMn = (ix) => { } return $('#topbar').ocmsmenu(m); } +/* Menu option CSV builder: 'setm' (Set-Preisanzeige) is only offered while the invoice has not + yet been explicitly switched to a set-display mode (admin.setmode empty/undefined) — once set, + changing it again is not supported (the user edits the line items directly instead); it + reappears only if 'Änderungen verwerfen' reverts to a DB state that was never switched. */ +$inv.emOpt = (admin) => 'iss,p13b,ctp' + (((admin || {}).setmode || '') === '' ? ',setm' : ''); $inv.eM = (r, re, opt) => { let m = []; if (booln(r, false) === true || booln(re, false) === true) { @@ -99,11 +104,507 @@ $inv.eM = (r, re, opt) => { if ((opt || '').split(',').includes('setm') === true) { m.push({ lbl: $ict.setm, fnc: $inv.ssetmode }); } + if ((opt || '').split(',').includes('iss') === true) { /* backend-authoritative draft (ADR 0006) */ + m.push({ lbl: 'Änderungshistorie', fnc: () => $inv.d.history() }); + m.push({ lbl: 'Änderungen verwerfen', fnc: () => $inv.d.discard() }); + } if (booln(r, false) === true) { m.push({ lbl: $ict.rel, fnc: $inv.rReload }); } return $('#topbar').ocmsmenu(m); /* empty Array => empty menu */ }; +/* ── Backend-authoritative draft editing controller (ADR 0006/0007) ─────────── + The server holds the truth for an invoice draft in an in-memory session; this + object seeds it, sends single edits as deltas, and renders the totals footer + + validation from the authoritative server state. It is invoice-only: reminders + never obtain a token (invSumUpdate, the seed trigger, is only bound for invoices), + so they keep the legacy stateless flow untouched. */ +$inv.d = { + tbl: () => $('div.invoice_layout table.invi'), + layout: () => $('div.invoice_layout'), + token: function () { return $inv.d.tbl().data('dtoken') || ''; }, + /* Hash each assembled block so only genuinely-changed blocks are sent as deltas. */ + hashes: function () { + let bai = $inv.d.tbl().data('bai') || [], h = {}; + $.each(bai, (i, b) => { h[(b.Id || '').toString()] = JSON.stringify(b); }); + return h; + }, + /* Seed the authoritative server session from the assembled editor payload. Called + once from invSumUpdate on open; thereafter the backend is the source of truth. + The in-flight ajax promise is stashed on 'dseedpromise' so a delta sent (e.g. via + a context-menu action) before seeding has actually completed can wait for the token + instead of silently no-op'ing (see $inv.d.sync). */ + seed: function (payload) { + let l = $inv.d.layout(); l.aC('freeze'); + let p = $ocms.postXT({ + url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => { + $inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order()); + $fis.draft.bind(r.token, { + onReady: () => $inv.d.refresh(), + onExpiring: (s) => $inv.d.warnExpiry(s), + onClosed: (reason) => $inv.d.closed(reason) + }); + $inv.d.refresh(); + }, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding').removeData('dseedpromise'); } + }); + $inv.d.tbl().data('dseedpromise', p); + return p; + }, + /* Re-fetch the authoritative state and render the totals footer + validation from it. */ + refresh: function (cb) { + let t = $inv.d.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('inv/dstate'), data: { token: t }, success: (state) => { + $inv.d.applyState(state); if (typeof cb === 'function') { cb(state); } + }, error: (xhr) => { if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }, + complete: () => { $inv.d.layout().rC('freeze'); } + }); + }, + applyState: function (state) { + let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; } + /* Concurrent refreshes (one per flushed block plus one for a follow-up delta such as + item.setprice, and SignalR draftReady on top) can have their dstate responses arrive + out of order. Applying a response older than the version already rendered would + silently roll back a just-applied server-side mutation (e.g. a set-price conversion + reverting to its pre-conversion 0/individually-priced state) — so stale responses are + dropped here instead of rendered. */ + let curVer = tbl.data('dver'); + if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) { + return; + } + tbl.data('dver', state.version).data('serverSums', state.sums); + $inv.d.footer(tbl, state.sums || {}, state.admin || {}); + $inv.d.notes(state.notes || []); + $inv.d.validation(state.validation || []); + $inv.d.applyItems(tbl, state.req || []); + $inv.d.applySetDisplay(tbl, state.setDisplay || {}); + /* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode: + hides it right after a mode switch, and restores it if a discard reverted the draft to + a DB state that was never switched (ADR 0006 backend-authoritative refresh). */ + $inv.eM(false, true, $inv.emOpt(state.admin || {})); + }, + /* Renders the backend-authoritative notice paragraphs (ADR: notice texts are + backend-authoritative) exactly as FuchsPdf.BuildInvoiceNotes emits them for the PDF, + so the online editor always shows precisely what will be printed. */ + notes: function (notes) { + let frm = $inv.d.layout(); if (frm.length < 1) { return; } + let box = frm.children('.dnotes'); + if (box.length < 1) { box = $$.dc('dnotes'); frm.append(box); } + box.empty(); + (notes || []).forEach((n) => $$.dc('dnote').aC(n.style || 'InvoiceNotes').text(n.text)); + }, + /* Push the server's authoritative position numbers back onto the rendered rows so the online + editor and the PDF preview always agree (the server numbers priced lines continuously; the + browser must not keep its own numbering). Only the position cell is touched — no re-render. */ + /* Reconciles each block's rendered rows against the authoritative server line list + (`req[].itm`, the co shape). The server may not only change a line's values but also + ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines + ("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of + that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert + the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept + distinct from 0). Rows the server no longer lists are removed; order follows the server. + Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */ + applyItems: function (tbl, req) { + let val = (x) => (x === undefined ? null : x); // JSON null stays null; absent -> null + (req || []).forEach((b) => { + if (!b) { return; } + let bid = (b.Id || '').toString(); if (bid === '') { return; } + let bdy = tbl.children('tbody').filter((i, e) => (($(e).data() || {}).Id || '').toString() === bid).first(); + if (bdy.length < 1) { return; } + let itms = b.itm || [], serverIds = {}; + let title = bdy.find('tr.title').first(); + let prev = title.length ? title : null; + itms.forEach((co) => { + let id = (co.id || '').toString(); if (id === '') { return; } + serverIds[id] = true; + let rw = bdy.find('#itm' + id).first(), isNew = rw.length < 1; + if (isNew) { rw = $$.tr(bdy, { id: 'itm' + id, class: 'itm' }); } + let dta = rw.data() || {}; + let changed = isNew || (dta.Type || '') !== (co.typ || '') + || dta.net !== val(co.v) || dta.net_val !== val(co.vt) || dta.vat_val !== val(co.vv) + || dta.svcnet_val !== val(co.vs) || dta.svcvat_val !== val(co.vsv) + || (dta.position || '') !== (co.p != null ? co.p : ''); + $.extend(dta, { + Id: id, Type: co.typ, + net: val(co.v), net_val: val(co.vt), vat_val: val(co.vv), + svcnet_val: val(co.vs), svcvat_val: val(co.vsv), + vat: co.vat, position: (co.p != null ? co.p : ''), + SetItmId: (co.SetItmId != null ? co.SetItmId : null) + }); + if (isNew) { $.extend(dta, { htmltext: co.t, quantity: co.q }); } // text only for server-created rows (e.g. the set row) + rw.data(dta); + if (prev) { if (rw.prev()[0] !== prev[0]) { rw.insertAfter(prev); } } // keep server order, but don't churn already-ordered rows + else if (bdy.children().first()[0] !== rw[0]) { bdy.prepend(rw); } + prev = rw; + if (changed) { $inv.rrw.call(rw); } + }); + bdy.find('tr.itm').each((i, tr) => { + let id = (($(tr).data() || {}).Id || '').toString(); + if (id !== '' && !serverIds[id]) { $(tr).remove(); } // dropped server-side (block.setonly) + }); + }); + }, + /* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay / + InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the + server says should show no price (set members in SetPrice/SetOnly mode), and emphasises + the set header line — so the online editor always shows exactly what the PDF will print, + without duplicating the pricing rules client-side. Rows not present in the map (no set in + that block, or standalone items) are left untouched. */ + applySetDisplay: function (tbl, setDisplay) { + tbl.find('tr.itm').each((i, tr) => { + let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString(); + if (id === '' || !Object.prototype.hasOwnProperty.call(setDisplay, id)) { return; } + let flags = setDisplay[id] || {}; + rw.tC('sethdr', bool(flags.h, false)); + if (bool(flags.p, true) === false) { + rw.find('td.currency').text(''); + } + }); + }, + /* Send one change to the server; the draftReady signal and this success both refresh. + Returns the underlying ajax promise so callers that must not race a follow-up delta + against this one (e.g. $inv.toSetPrice after a flush) can chain on it. + If the initial seeding (inv/dopen) is still in flight (no token yet, but a + 'dseedpromise' is pending), the delta previously vanished silently — the user saw no + network activity at all when clicking a context-menu action right after opening the + editor. Now the delta waits for the seed to finish and retries against the freshly + issued token instead of being dropped. */ + sync: function (delta) { + let t = $inv.d.token(); + if (t === '') { + let seeding = $inv.d.tbl().data('dseedpromise'); + if (seeding) { return $.when(seeding).then(() => $inv.d.sync(delta)); } + return $.when(); + } + $inv.d.layout().aC('freeze'); + return $ocms.postXT({ + url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) }, + success: () => { $inv.d.refresh(); }, + error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } } + }); + }, + /* The current section id sequence (used to detect a reorder that changes no block content). */ + order: function () { return (($inv.d.tbl().data('bai')) || []).map((b) => (b.Id || '').toString()); }, + /* Diff the freshly-rebuilt blocks against the last-synced state and send only the + changed/removed blocks as granular block.replace / block.remove deltas. A pure section + reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a + block.order delta; the server reorders the cache, renumbers positions and pushes them back. + No early return on an empty token here: sync() itself waits for an in-flight seed + (dseedpromise) instead of silently dropping the delta, so edits made in the brief window + before seeding completes are not lost. */ + syncChanged: function (tbl) { + let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [], pending = []; + $.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } }); + $.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } }); + let order = $inv.d.order(), prevOrder = tbl.data('dorder') || []; + tbl.data('dhashes', next).data('dorder', order); + changed.forEach((b) => pending.push($inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b }))); + removed.forEach((id) => pending.push($inv.d.sync({ Target: 'block.remove', Ref: id }))); + let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(','); + if (sameSet && prevOrder.join(',') !== order.join(',')) { pending.push($inv.d.sync({ Target: 'block.order', Value: order })); } + /* $.when(...pending) so a caller (e.g. toSetPrice) can wait until every flushed block + has actually reached the server before sending a further delta that depends on it — + otherwise a delayed block.replace carrying pre-conversion values can land after and + silently overwrite a just-applied server-side mutation (e.g. item.setprice). */ + return $.when.apply($, pending).promise(); + }, + /* Map an inline recipient field to its delta target and send it. No token-guard here for the + same reason as syncChanged: sync() waits for an in-flight seed instead of dropping the edit. */ + syncField: function (nme, val) { + let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' }; + let target = map[nme]; if (!target) { return; } + $inv.d.sync({ Target: target, Value: val }); + }, + /* Render the totals footer from the server sums (port of invSumUpdate's footer half). */ + footer: function (tbl, sums, admin) { + let ft = tbl.children('tfoot').empty(); tbl.nextAll('.fnote').remove(); + let p13b = bool(admin.p13b, false); + let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst)); + let fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t); + rwcy('Netto', sums.total_net || 0); + if (p13b === false) { $.each(sums.vat || {}, (rate, amt) => rwcy($rct.vat + ' ' + rate + '%', amt, 'tvat')); } + rwcy('Summe', sums.total_gross || 0); + let itype = (admin.type || ''); + if (itype === 'i') { fn($rct.note2); fn($rct.note4); } + else if (itype === 'c') { fn($rct.note2); } + else { + fn(string($rct.note3, [fnum(((sums.service_net || 0) + (sums.service_vat || 0)) * (admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax'); + fn($rct.note2); + fn(string($rct.note1, [fnum((sums.service_net || 0) + (sums.service_vat || 0), $rct.cst), fnum(sums.service_net || 0, $rct.cst), fnum(sums.service_vat || 0, $rct.cst)])); + } + if (p13b === true) { fn($rct.note13b); } + }, + validation: function (msgs) { + let frm = $('div.invoice_layout'); if (frm.length < 1) { return; } + let box = frm.children('.dvalidation'); + if (box.length < 1) { box = $$.dc('dvalidation'); frm.prepend(box); } + box.empty().tC('hidden', (msgs || []).length < 1); + $.each(msgs || [], (i, m) => $$.dc('dvmsg', box).aC(m.severity).text(m.message)); + }, + /* PDF preview straight from the cache; confirm = flush + finalise, cancel = discard. */ + preview: function () { + let t = $inv.d.token(); if (t === '') { return; } + let l = $inv.d.layout(); + let email = ($inv.d.tbl().data('new') || {}).invoiceemail || ''; + if ($fis.ValidateEmail(email) === false) { if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { return; } } + l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('inv/dpreview'), data: { token: t }, success: (response) => { + l.rC('freeze'); + let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), total = response.total; + if (total > 10) { $$.dc('note warn', c).text($ict.tpe); } + $.each(response.img || [], (ii, img) => { $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); }); + for (let ic = (response.img || []).length + 1; ic <= total; ic++) { $$.dc('pdfp ph', c).append($$.dc('note', $ict.pna)); } + $ocms.dlg(c, { + size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $rct.crI, + confirm: function (e) { + let ct = $(this); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('inv/dsave'), data: { token: t }, success: (sv) => { + $ocms.postXT({ + url: $ocms.url('req/sconf'), data: { id: sv.invid }, success: (cresp) => { + ct.trigger('modal_close'); + if (cresp.hasFile === true) { window.open($ocms.url('req/idoc') + '?id=' + sv.invid, '_blank'); } + $inv.d.close(); + $ocms.init('req'); $inv.rReload(); + }, error: () => { alert($t.f1); ct.trigger('modal_close'); }, complete: () => { l.rC('freeze'); } + }); + }, error: () => { l.rC('freeze'); alert($ict.eis); } + }); + }, + cancel: function (e) { if (confirm($ict.cdI)) { $inv.d.close(); $inv.rReload(); } } + }); + }, error: () => { l.rC('freeze'); alert($ict.eis); } + }); + }, + /* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */ + save: function () { + let t = $inv.d.token(); if (t === '') { return; } + let l = $inv.d.layout(); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('inv/dsave'), data: { token: t }, success: (r) => { $inv.d.tbl().data('invid', r.invid); }, + error: () => { alert($ict.eis); }, complete: () => { l.rC('freeze'); } + }); + }, + history: function () { + let t = $inv.d.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('inv/dhistory'), data: { token: t }, success: (r) => { + let c = $$.dc('dhist'); + if ((r.history || []).length < 1) { $$.dc('note', c).text('Noch keine Änderungen erfasst.'); } + else { + let ts = $$.tblset({ class: 'invtbl fullwidth' }, c); + $$.tr(ts.hd).append([$$.th().text('Zeit'), $$.th().text('Feld'), $$.th().text('Alt'), $$.th().text('Neu')]); + $.each(r.history, (i, h) => $$.tr(ts.bdy).append([$$.tdc('keep', fdt(h.timestamp)), $$.td().text(h.target), $$.td().text(h.oldValue), $$.td().text(h.newValue)])); + } + $ocms.dlg(c, { width: 800, form: false }); + } + }); + }, + discard: function () { + let invid = $inv.d.tbl().data('invid') || ''; + if (invid === '') { alert('Es wurde noch kein Zwischenstand gespeichert, der wiederhergestellt werden könnte.'); return; } + if (confirm('Alle Änderungen verwerfen und den zuletzt gespeicherten Stand neu laden?') === false) { return; } + $inv.d.close(); + $inv.cntInv({ id: invid }); + }, + warnExpiry: function (secondsLeft) { + let mins = Math.max(1, Math.round((secondsLeft || 0) / 60)); + $fis.notifications.push({ severity: 'info', title: 'Entwurf läuft ab', message: 'Der Rechnungsentwurf läuft in etwa ' + mins + ' Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren.' }); + }, + closed: function (reason) { + let t = $inv.d.token(); + $inv.d.tbl().removeData('dtoken'); + if (t !== '') { $fis.draft.release(t); } + $fis.frm_edit().remove(); $fis.lf(true); + $fis.notifications.push({ severity: 'error', title: 'Entwurf geschlossen', message: reason === 'expired' ? 'Der Rechnungsentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.' : 'Der Rechnungsentwurf wurde geschlossen.' }); + try { $inv.rReload(); } catch (e) { } + }, + close: function () { + let t = $inv.d.token(); + if (t !== '') { $ocms.postXT({ url: $ocms.url('inv/dclose'), data: { token: t } }); $fis.draft.release(t); } + $inv.d.tbl().removeData('dtoken'); + } +}; +/* ── Backend-authoritative reminder draft editing (ADR 0006/0007) ───────────── + The reminder mirror of $inv.d: the server holds the truth for a reminder draft in + an in-memory session; this object seeds it (rem/dopen), sends single edits as deltas + (rem/dpatch), and renders the open-amount footer + validation from the authoritative + server state (rem/dstate). Preview renders straight from the cache (rem/dpreview); + confirm flushes (rem/dsave) then finalises + emails (rem/conf). It coexists with $inv.d + on the same DOM: each keys off its own token (rdtoken vs dtoken), so the shared inline + editor safely no-ops for the mode that is not active. */ +$inv.rd = { + tbl: () => $('div.invoice_layout table.invi'), + layout: () => $('div.invoice_layout'), + token: function () { return $inv.rd.tbl().data('rdtoken') || ''; }, + /* Seed the authoritative server session from the assembled reminder editor payload. */ + seed: function (payload) { + let l = $inv.rd.layout(); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => { + $inv.rd.tbl().data('rdtoken', r.token).data('rdver', r.version); + $fis.draft.bind(r.token, { + onReady: () => $inv.rd.refresh(), + onExpiring: (s) => $inv.rd.warnExpiry(s), + onClosed: (reason) => $inv.rd.closed(reason) + }); + $inv.rd.refresh(); + }, error: () => { l.rC('freeze'); } + }); + }, + /* Re-fetch the authoritative state and render the open-amount footer + validation from it. */ + refresh: function (cb) { + let t = $inv.rd.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('rem/dstate'), data: { token: t }, success: (state) => { + $inv.rd.applyState(state); if (typeof cb === 'function') { cb(state); } + }, error: (xhr) => { if (xhr && xhr.status === 410) { $inv.rd.closed('expired'); } }, + complete: () => { $inv.rd.layout().rC('freeze'); } + }); + }, + applyState: function (state) { + let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; } + /* See $inv.d.applyState — drop stale/out-of-order dstate responses so an older refresh + can never roll back a newer one's totals/validation. */ + let curVer = tbl.data('rdver'); + if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) { + return; + } + tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || ''); + $inv.rd.footer(tbl, state.sums || {}); + $inv.rd.notes(state.notes || {}); + $inv.rd.validation(state.validation || []); + }, + /* Renders the backend-authoritative intro/closing notice paragraphs (ADR: notice texts + are backend-authoritative) exactly as FuchsPdf.BuildReminderNotes emits them for the + PDF, so the online editor always shows precisely what will be printed. */ + notes: function (notes) { + let frm = $inv.rd.layout(); if (frm.length < 1) { return; } + let rq = frm.find('.srq'); if (rq.length < 1) { return; } + rq.children('.dnote').remove(); + let intro = (notes || {}).intro || [], first = rq.children().first(); + intro.slice().reverse().forEach((t) => { + let p = $$[0]('p').aC('dnote intro').rwText(t); + if (first.length) { p.insertBefore(first); } else { p.appendTo(rq); } + }); + ((notes || {}).closing || []).forEach((t) => $$[0]('p').aC('dnote closing').rwText(t).appendTo(rq)); + }, + /* Send one change to the server; the draftReady signal and this success both refresh. */ + sync: function (delta) { + let t = $inv.rd.token(); if (t === '') { return; } + $inv.rd.layout().aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dpatch'), data: { token: t, delta: JSON.stringify(delta) }, + success: () => { $inv.rd.refresh(); }, + error: (xhr) => { $inv.rd.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.rd.closed('expired'); } } + }); + }, + /* Map an inline recipient field to its delta target and send it. */ + syncField: function (nme, val) { + if ($inv.rd.token() === '') { return; } + let map = { subject: 'subject', invoiceaddress: 'address', invoiceemail: 'email', text: 'text' }; + let target = map[nme]; if (!target) { return; } + $inv.rd.sync({ Target: target, Value: val }); + }, + /* Amount / amount-paid come from the item-row dialog; send both as their own deltas. */ + syncAmount: function (amount, amount_payed) { + if ($inv.rd.token() === '') { return; } + $inv.rd.sync({ Target: 'amount', Value: (amount != null ? amount : 0).toString() }); + $inv.rd.sync({ Target: 'amount_payed', Value: (amount_payed != null ? amount_payed : 0).toString() }); + }, + /* Render the open-amount footer from the server sums. Detail rows ($inv.rRemRw) have + 6 columns (aux, invoiceid, date, amount, amount_payed, open); span the label over the + 4 columns between aux and the open-amount column so the total lands in the same last + column as the per-invoice open amounts. */ + footer: function (tbl, sums) { + let ft = tbl.children('tfoot').empty(); + let tr = $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Offener Betrag')]); + $$.tdc('currency', tr, fnum(sums.amount_open || 0, $rct.cst)); + }, + validation: function (msgs) { + let frm = $inv.rd.layout(); if (frm.length < 1) { return; } + let box = frm.children('.dvalidation'); + if (box.length < 1) { box = $$.dc('dvalidation'); frm.prepend(box); } + box.empty().tC('hidden', (msgs || []).length < 1); + $.each(msgs || [], (i, m) => $$.dc('dvmsg', box).aC(m.severity).text(m.message)); + }, + /* PDF preview straight from the cache; confirm = flush + finalise + email, cancel = discard. */ + preview: function () { + let t = $inv.rd.token(); if (t === '') { return; } + let l = $inv.rd.layout(); + let email = (($inv.rd.tbl().data('new') || {}).invoiceemail) || ''; + if ($fis.ValidateEmail(email) === false) { if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { return; } } + l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dpreview'), data: { token: t }, success: (response) => { + l.rC('freeze'); + let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88); + $.each(response.img || [], (ii, img) => { $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); }); + $ocms.dlg(c, { + size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $ict.remd, + confirm: function (e) { + let ct = $(this); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dsave'), data: { token: t }, success: (sv) => { + $ocms.postXT({ + url: $ocms.url('rem/conf'), data: { id: sv.remid }, success: () => { + ct.trigger('modal_close'); + window.open($ocms.url('rem/idoc') + '?id=' + sv.remid, '_blank'); + $inv.rd.close(); + $ocms.init('req'); $inv.rReload(); + }, error: () => { alert($t.f1); ct.trigger('modal_close'); }, complete: () => { l.rC('freeze'); } + }); + }, error: () => { l.rC('freeze'); alert($t.f1); } + }); + }, + cancel: function (e) { if (confirm($ict.cdI)) { $inv.rd.close(); $inv.rReload(); } } + }); + }, error: () => { l.rC('freeze'); alert($t.f1); } + }); + }, + /* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */ + save: function () { + let t = $inv.rd.token(); if (t === '') { return; } + let l = $inv.rd.layout(); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dsave'), data: { token: t }, success: (r) => { $inv.rd.tbl().data('remid', r.remid); }, + error: () => { alert($t.f1); }, complete: () => { l.rC('freeze'); } + }); + }, + history: function () { + let t = $inv.rd.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('rem/dhistory'), data: { token: t }, success: (r) => { + let c = $$.dc('dhist'); + if ((r.history || []).length < 1) { $$.dc('note', c).text('Noch keine Änderungen erfasst.'); } + else { + let ts = $$.tblset({ class: 'invtbl fullwidth' }, c); + $$.tr(ts.hd).append([$$.th().text('Zeit'), $$.th().text('Feld'), $$.th().text('Alt'), $$.th().text('Neu')]); + $.each(r.history, (i, h) => $$.tr(ts.bdy).append([$$.tdc('keep', fdt(h.timestamp)), $$.td().text(h.target), $$.td().text(h.oldValue), $$.td().text(h.newValue)])); + } + $ocms.dlg(c, { width: 800, form: false }); + } + }); + }, + warnExpiry: function (secondsLeft) { + let mins = Math.max(1, Math.round((secondsLeft || 0) / 60)); + $fis.notifications.push({ severity: 'info', title: 'Entwurf läuft ab', message: 'Der Mahnentwurf läuft in etwa ' + mins + ' Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren.' }); + }, + closed: function (reason) { + let t = $inv.rd.token(); + $inv.rd.tbl().removeData('rdtoken'); + if (t !== '') { $fis.draft.release(t); } + $fis.frm_edit().remove(); $fis.lf(true); + $fis.notifications.push({ severity: 'error', title: 'Entwurf geschlossen', message: reason === 'expired' ? 'Der Mahnentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.' : 'Der Mahnentwurf wurde geschlossen.' }); + try { $inv.rReload(); } catch (e) { } + }, + close: function () { + let t = $inv.rd.token(); + if (t !== '') { $ocms.postXT({ url: $ocms.url('rem/dclose'), data: { token: t } }); $fis.draft.release(t); } + $inv.rd.tbl().removeData('rdtoken'); + } +}; $inv.cInv2 = function (data) { let fr = $$.dc('rfrm').ldng(1); let o = $ocms.dlg(fr, { width: 1000 }); @@ -261,7 +762,7 @@ $inv.ccInv = function (ev) { //normale rechnung rif.tbl.children('tbody').each($inv.bdysort); rif.tbl.trigger('fds.inv'); /* trigger calculations */ - $inv.eM(false, true, 'iss,p13b,setm,ctp'); + $inv.eM(false, true, $inv.emOpt(admin)); }, complete: () => { o.c.trigger('modal_close'); } @@ -406,7 +907,7 @@ $inv.cntInv = function (data) { //invoice continuation rif.tbl.children('tbody').each($inv.bdysort); rif.tbl.trigger('fds.inv'); /* trigger calculations */ - $inv.eM(false, true, 'iss,p13b,setm,ctp'); + $inv.eM(false, true, $inv.emOpt(response.admin)); }, complete: () => { o.c.trigger('modal_close'); } @@ -481,9 +982,12 @@ $inv.cSt = function (data) { }; $inv.eHtml = function (ev) { let t = $(this), frmct = ev.data instanceof jQuery ? ev.data : ev.data.t; - /* invoiceemail must stay plain text — using the TinyMCE/html editor here used to wrap the - address in

tags, which then got posted and persisted verbatim into SendToEmail. */ - let isPlainText = ev.data.nme === 'invoiceemail'; + /* Single-line fields must stay plain text — the TinyMCE/html editor wraps the value in

+ tags, which used to get posted and persisted verbatim (e.g.

18.06.2026

in the + Leistungsdatum). The backend sanitises HTML too (single source of truth, ADR 0006), but + keeping these plain here avoids the UI briefly holding the wrapped value. Multi-line fields + (invoiceaddress, loc) stay HTML-capable and are normalised to newlines server-side. */ + let isPlainText = ['invoiceemail', 'provisionperiod', 'invoicetitle'].includes(ev.data.nme); let flds = isPlainText ? [{ name: 'txt', label: 'Text', type: 'text', value: frmct.text() }] : [{ name: 'txt', label: 'Text', type: 'html', value: frmct.html(), tinymce: true, attr: { style: 'height: 300px' } }]; @@ -499,6 +1003,11 @@ $inv.eHtml = function (ev) { if (typeof change === 'function') { change(response.txt); } + /* backend-authoritative: mirror the inline recipient-field edit to the server session. + Invoice and reminder editors share this DOM; each syncField no-ops unless its own + draft token is present, so only the active mode's session receives the delta. */ + $inv.d.syncField(ev.data.nme, isPlainText ? (response.txt || '') : response.txt); + $inv.rd.syncField(ev.data.nme, isPlainText ? (response.txt || '') : response.txt); }, tinymce: { valid_elements: 'br', hidemenu: true, hidetoolbar: true } } @@ -521,6 +1030,8 @@ $inv.eHtml = function (ev) { } $ocms.dlgform(flds, sets); }; +/* No client-side arithmetic (ADR 0006/0008): only the chosen VAT rate is stored on each row; + the server (InvoiceDraftCalculator.RecomputeLineValues) recomputes vat_val/svcvat_val from it. */ $inv.setVat = function (ev) { let t = $(this), thisrow = ev.data; let vat = prompt($rct.rqV); @@ -533,12 +1044,6 @@ $inv.setVat = function (ev) { thisrow.siblings('.itm').each(function () { let rwi = $(this), dta = rwi.data(); dta.vat = fnum(vat, { style: 'percent' }).replace(' ', ''); - if ((dta.net_val || 0) > 0) { - dta.vat_val = dta.net_val * vat; - } - if ((dta.svcnet_val || 0) > 0) { - dta.svcvat_val = dta.svcnet_val * vat; - } }); $inv.t_fds_inv(); } @@ -584,7 +1089,11 @@ $inv.eRw = function(row, dta, flds) { }, typedvalues: true }); }; -$inv.bdysort = (i, e) => { $(e).Sortable({ dragItem: false, dragHandleClass: 'ico', parentident: 'tr', swapdone: (p1, p2, i1, i2) => { $inv.t_fds_inv(); } }) } +/* Reorder items via drag. The DOM swap happens inside the Sortable during the drag; we commit + the new order once, reliably, on drop (onend) — that recomputes positions/totals and pushes the + changed block(s) to the backend session (t_fds_inv -> syncChanged). Committing on drop (rather + than on every mid-drag hover-swap) avoids rebuilding the row that is currently being dragged. */ +$inv.bdysort = (i, e) => { $(e).Sortable({ dragItem: false, dragHandleClass: 'ico', parentident: 'tr', onend: () => { $inv.t_fds_inv(); } }) } $inv.rrw = function () { let rw = $(this), dta = rw.data(), co = {}, ph = rw.is('.placeholder'), hn = rw.is('.hidenote'); let oHtml = (e) => $$.d().append(e).html(); @@ -594,6 +1103,16 @@ $inv.rrw = function () { if (ph === false) { bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow)); bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } })); + /* "Auf Setpreis umstellen": offered on a Set header item that does not yet carry a + price. fds__prepInvoice's [SetItmId] now anchors on the still-unconverted (price 0) + header, so the header row's own SetItmId self-references its own id rather than being + null — the real signal for showing this button is still the header's own price/net_val + being 0 (once converted it carries a nonzero price and the button disappears). + Irreversible: the actual sum/clear mutation happens server-side (backend-authoritative, + see InvoiceDraftEditService.ApplyItemSetPrice) via the "item.setprice" patch target. */ + if ((dta.Type || '').toLowerCase() === 'set' && (dta.net_val || 0) === 0) { + bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice)); + } } let axf = $$.dc('axf').append(bc); if (ph === true) { @@ -601,7 +1120,7 @@ $inv.rrw = function () { } else if (rw.is('.itm.osum') === true) { co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false }; } else { - co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false }; + co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false, SetItmId: dta.SetItmId || null }; $$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */ co.p = dta.position || (dta.SortOrder || ''); @@ -611,7 +1130,7 @@ $inv.rrw = function () { co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || '')); } else { co.tt = co.det ? '' : $$.s(dta.Note || '').text(); - co.q = dta.quantity || (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')); + co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */ co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || ''))); co.v = dta.net co.vt = dta.net_val @@ -630,45 +1149,62 @@ $inv.rrw = function () { tda.push($$.td(rw, { colspan: 4 }).append(co.t)); } else { Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []); + /* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis" + members, and the set row's own unit-price), distinct from a real 0,00 €. */ + let cur = (x) => (x == null ? '' : fnum(x, $rct.cst)); Array.prototype.push.apply(tda, [ $$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t), - $$.tdc('currency').text(fnum(co.v, $rct.cst)), - $$.tdc('currency inetval').text(fnum(co.vt, $rct.cst)).attr('title', $rct.svcPart + ': ' + fnum(co.vs, $rct.cst)) + $$.tdc('currency').text(cur(co.v)), + $$.tdc('currency inetval').text(cur(co.vt)).attr('title', $rct.svcPart + ': ' + cur(co.vs)) ]); } - rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ).tC('hidenote', hn).append(tda); + rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ) + .tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */ + .tC('hidenote', hn).append(tda); dta.co = co; }; +/* "Auf Setpreis umstellen" (see $inv.rrw): backend-authoritative (ADR 0006) — the browser only + posts the target set-header item id; the server (InvoiceDraftEditService.ApplyItemSetPrice) + sums the member rows (same block, [SetItmId] equal to this row's id), writes that sum onto + the set header and clears each member's price, then the normal draftReady refresh re-renders + both the header and its members from the authoritative session state. This mirrors the PDF's + read of the same cached session, so the online editor and PDF can never drift apart. */ +$inv.toSetPrice = function (ev) { + let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString(); + if (id === '') { return; } + if (confirm($rct.toSetPc) === false) { return; } + /* Wait for the flush of any pending local edits to actually reach the server before sending + the conversion delta — sending both in parallel let a delayed block.replace (still carrying + the pre-conversion, zero member/header values) land after item.setprice and silently + overwrite the just-converted totals back to 0. */ + $.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: 'item.setprice', Ref: id }); }); +}; +/* No client-side totals/footer/tax computation (ADR 0006/0008): this only re-assembles each + block's row contract array (needed to post the "req" shape to the server) and, on the very + first pass, seeds the authoritative backend session. Footer, VAT breakdown, service-refund + note figures and the per-block sum cell are never computed here — they are rendered exclusively + from the server's `dstate` response by $inv.d.footer / $inv.d.applyState once the draft has a + token. Before a token exists the footer/notes/isum cells are simply left empty; there is no + local approximation to display. */ $inv.invSumUpdate = function () { - let tbl = $(this), ft = tbl.children('tfoot').empty(), p13b = bool((tbl.data().admin || {}).p13b || '', false); - tbl.nextAll('.fnote').remove(); - let sms = { ttn: 0, ttb: 0, ttvat: 0, tscn: 0, tscvat: 0, vat: {}, itmnet: {} }, ba = []; - let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst)), fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t); - let csms = function (rrx, sms, sid) { - sms.tscn += (rrx.svcnet_val || 0); - sms.tscvat += (rrx.svcvat_val || 0); - sms.ttn += (rrx.net_val || 0); - sms.ttvat += (rrx.vat_val || 0); - sms.ttb += ((rrx.net_val || 0) + (rrx.vat_val || 0)); - if ((rrx.vat || '') !== '') { - sms.vat[rrx.vat] = (sms.vat[rrx.vat] || 0) + (rrx.vat_val || 0); - } - //sms.itmnet[sid] = (sms.itmnet[sid] || 0) + (rrx.net_val || 0); - }; + let tbl = $(this); + let ba = []; let bds = tbl.children('tbody'); bds.each((bi, bdy) => { - let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0; + let b = $(bdy), rx = b.data() || {}, i = [], citems = [], itm = b.find('tr.itm'), iso = 0, ipos = 0; b.tC('empty', itm.length < 1); itm.each((ti, tx) => { - - let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co); - //console.debug('rrx %o', rrx); + let rrx = $(tx).data() || {}; + i.push(rrx.co); /* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing. - Set grouping: an item of Type 'set' is a header that claims the following items in this - block as its members until the next set header (mfr__items has no explicit member link). */ + Set grouping: fds__prepInvoice computes [SetItmId] per item, anchored on the still-unconverted + (price 0) Set header that owns it — this is the authoritative membership signal (not row order), + so items not tagged with a SetItmId by the server are never swept into a preceding set. The + header row's own SetItmId self-references its own id, but it is never its own member (excluded + by the id !== '' && !== citem.id check below). */ let citem = $inv.itemToContract(rrx); - if (citem.type === 'set' && citem.id !== '') { cset = citem.id; } - else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; } + let sid = (rrx.SetItmId || '').toString(); + if (citem.type !== 'set' && sid !== '' && sid !== citem.id) { citem.setId = sid; } citems.push(citem); if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) { if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; } @@ -677,42 +1213,18 @@ $inv.invSumUpdate = function () { $inv.rrw.call(tx); } }); - //console.debug('%o', { - // f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet - //}); - b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst)); - ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet }); + ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems }); }); let nonempty = tbl.find('tbody:not(.empty)').length; bds.find('tr.isum').tC('hidden', nonempty < 2); - //let fnet = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Netto')]), fnum(sms.ttn, $rct.cst)); - rwcy('Netto', sms.ttn); - if (p13b === false) { - $.each(sms.vat, (vi, vx) => { - //$$.tdc('currency vat', $$.tr(ft, { class: 'tvat' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Umsatzsteuer ' + vi)]), fnum(vx, $rct.cst)); - rwcy($rct.vat + ' ' + vi, vx, 'tvat'); - }); - } else { - sms.ttb = sms.ttn; - } - //let fsum = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Summe')]), fnum(sms.ttb, $rct.cst)); - rwcy('Summe', sms.ttb); - let itype = tbl.data().admin.type; - if (itype === 'i') { - fn($rct.note2); - fn($rct.note4); - } else if (itype === 'c') { - fn($rct.note2); - } else { - fn(string($rct.note3, [fnum((sms.tscn + sms.tscvat) * (tbl.data().admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax'); - fn($rct.note2); - fn(string($rct.note1, [fnum(sms.tscn + sms.tscvat, $rct.cst), fnum(sms.tscn, $rct.cst), fnum(sms.tscvat, $rct.cst)])); - } - if (p13b === true) { - fn($rct.note13b); - } - tbl.data('sms', sms); tbl.data('bai', ba); + /* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice + draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled + model to the server session and switch to server-driven totals from then on. */ + if ((tbl.data('dtoken') || '') === '' && bool(tbl.data('dseeding'), false) === false && ((tbl.data('admin') || {}).type != null)) { + tbl.data('dseeding', true); + $inv.d.seed($.extend($inv.invcPayload(tbl.data()), { invid: tbl.data('invid') || '' })); + } }; $inv.worknotes = function (rx) { let wn = ''; @@ -800,7 +1312,21 @@ $inv.rendersrq = function () { $$.tdc('currency isumval', istr); } }; -$inv.t_fds_inv = () => { $('div.invoice_layout table.invi').trigger('fds.inv'); }; +$inv.t_fds_inv = () => { + let tbl = $('div.invoice_layout table.invi'); + tbl.trigger('fds.inv'); + /* After any local item mutation, push the changed block(s) to the authoritative + server session as granular deltas (invoice drafts only — reminders have no token, + nor a 'dseedpromise', so they correctly fall through to the no-op). While the initial + seed (inv/dopen) is still in flight there is no token yet either, but a 'dseedpromise' + is pending — wait for it instead of skipping the flush, otherwise an edit made in that + brief window is silently never sent. Returns the flush promise so callers that must + sequence a follow-up server delta after this flush (e.g. $inv.toSetPrice) can wait for + it instead of racing it. */ + if ((tbl.data('dtoken') || '') !== '') { return $inv.d.syncChanged(tbl); } + let seeding = tbl.data('dseedpromise'); + return seeding ? $.when(seeding).then(() => $inv.d.syncChanged(tbl)) : $.when(); +}; $inv.sedit = () => { $inv.sprev(true); }; @@ -855,6 +1381,7 @@ $inv.sp13b = () => { } else { } tbl.trigger('fds.inv'); + $inv.d.sync({ Target: 'p13b', Value: d.admin.p13b }); }; /* Maps an item row's data to the backend item contract consumed by InvoiceSetPricing / FuchsPdf.ApplyInvoice: { id, type, title (plain), desc (html), qty, price_net, @@ -865,7 +1392,10 @@ $inv.itemToContract = function (rrx) { rrx = rrx || {}; let oHtml = (e) => $$.d().append(e).html(); let type = (rrx.Type || '').toString().toLowerCase(); - let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' }; + /* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so + block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */ + let nz = (x) => (x == null ? null : x); + let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' }; if (rrx.co && rrx.co.typ === 'osum') { /* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */ ci.desc = rrx.co.t || ''; @@ -886,29 +1416,35 @@ $inv.itemToContract = function (rrx) { ci.desc = rrx.Note || ''; } ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : ''); - ci.price_net = (rrx.net || 0); - ci.total_net = (rrx.net_val || 0); + ci.price_net = nz(rrx.net); + ci.total_net = nz(rrx.net_val); } return ci; }; -/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode, - which BuildInvoiceParams turns into the "setmode:" InvoiceOptions token the PDF reads. */ +/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto + admin.setmode, which BuildInvoiceParams turns into the "setmode:" InvoiceOptions token + the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was + always just the implicit pre-conversion default and is not a state the user can switch back to + once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice / + InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */ $inv.ssetmode = () => { let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(); d.admin = d.admin || {}; let cur = (d.admin.setmode || 'setprice'), o; let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); }); - let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]); + let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]); o = $ocms.dlg(fr, { width: 800 }); }; +/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible, + backend-authoritative mutation grouped by service-request block. The server inserts a dedicated + set row per block (block sum) and either nulls each member's price (setprice) or removes the + members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The + browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace + can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */ $inv.setSetmode = (mode) => { - let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(); - d.admin = d.admin || {}; - d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */ - d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */ - let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0); - if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); } - d.inv.InvoiceOptions = opts.join(','); + if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; } + let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice'; + $.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: target }); }); }; $inv.sctp = () => { let flds = $invcol.ctp; @@ -925,126 +1461,45 @@ $inv.sctp = () => { cvo.contactEmail = response.email; d.new.CustomValues = JSON.stringify(cvo); //Assign it to new Values so that this is submitted also l.find('.ctpfrm').text(ne(response.name, response.email)); + $inv.d.sync({ Target: 'contact', Value: { name: response.name, email: response.email } }); }, typedvalues: true }); }; /* Normalises the editor's working model into the exact field names the C# backend - (FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload: - - balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross; - - every VAT rate's net amount is exposed as new.vat__net (the backend reads the highest); + (FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload. No totals/VAT + are computed here (ADR 0006/0008: the online editor performs no arithmetic at all) — the + backend session computes total_net/total_gross/vat itself (InvoiceDraftCalculator) once + opened from this payload, and BuildFdsData/BuildInvoiceParams reads them from there: - new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms -> new.paymentterm, admin.CustomerId -> admin.customerid. Originals are kept alongside; the source objects are not mutated. */ $inv.invcPayload = function (d) { d = d || {}; - let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin); - nw.total_net = sms.ttn || 0; - nw.total_gross = sms.ttb || 0; - /* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map - (FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */ + let nw = $.extend({}, d.new), adm = $.extend({}, d.admin); nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || '')); nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || '')); nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || '')); adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId); - return { admin: adm, req: d.bai, sms: d.sms, new: nw }; -}; -$inv.ssave = () => { - var l = $('div.invoice_layout'), d = l.find('table.invi').data(); - $inv.t_fds_inv(); - l.aC('freeze'); - $ocms.postXT({ - url: $ocms.url('req/save'), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid || '' }, success: (response) => { - $inv.cntInv({ id: response.id }); - }, error: () => { - alert($ict.eis); - }, complete: () => { - l.rC('freeze'); - } - }); -}; -$inv.sprev = (change) => { - var l = $('div.invoice_layout'), d = l.find('table.invi').data(); - change = bool(change, false); - $inv.t_fds_inv(); - l.aC('freeze'); - //console.debug({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }); - if ($fis.ValidateEmail(d.new.invoiceemail || '') === false) { - if (bool(confirm($ict.ivE + $ict.ivEc),false) === false) { - l.rC('freeze'); - return; - } - } - $ocms.postXT({ - url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid ||'' }, success: (response) => { - let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id, invtp = response.total; - if (invtp > 10) { - $$.dc('note warn', c).text($ict.tpe); - } - $.each(response.img || [], function (ii, img) { - $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); - }); - for (let ic = (response.img || []).length + 1; ic <= invtp; ic++) { - $$.dc('pdfp ph', c).append($$.dc('note', $ict.pna)); - } - $ocms.dlg(c, { - size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $rct.crI, confirm: function (e) { - let ct = $(this); - l.aC('freeze'); /* spinner while the invoice is finalized/emailed on the backend */ - $ocms.postXT({ - url: $ocms.url('req/sconf'), data: { id: invid }, success: (cresp) => { - ct.trigger('modal_close'); - if (cresp.hasFile === true) { - window.open($ocms.url('req/idoc') + '?id=' + invid, '_blank'); /* open pdf in new tab, only if a file was actually created */ - } - $ocms.init('req'); /* go back to request list */ - $inv.rReload(); - }, error: () => { - alert($t.f1); - ct.trigger('modal_close'); - }, complete: () => { - l.rC('freeze'); - } - }); - }, cancel: function (e) { - let ct = $(this); - if (confirm($ict.cdI)) { - $ocms.postXT({ - url: $ocms.url('req/sdel'), data: { - id: invid - } - }); - } - $inv.rReload(); - } - }); - }, error: () => { - alert($ict.eis); - }, complete: () => { - l.rC('freeze'); - } - }); + return { admin: adm, req: d.bai, new: nw }; }; +/* Zwischenspeichern and preview now run against the backend-authoritative session + (ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by + token. See $inv.d.save / $inv.d.preview. The finalise + email path (req/sconf) is + unchanged and is invoked from the preview modal's confirm handler. */ +$inv.ssave = () => { $inv.d.save(); }; +$inv.sprev = (change) => { $inv.d.preview(); }; $inv.rReload = () => { try { let s = $('#listframe ul.rql:first').data(); $inv.cInv2({ id: s.search }); } catch (e) { } }; -$inv.quantChange = function (i) { - //console.debug({ t: this, i: i }); - let t = $(this), f = t.closest('form'), fi = {}, pf = (i) => parseFloat(i.toString().replace('%', '').replace(',', '.')), rtp = (num) => num.toFixed(2); - f.find(':input').each((i, e) => { fi[$(e).attr('name')] = $(e); }); - let qv = parseInt(fi.quantityhours.val() || '0'), nv = pf(fi.net.val() || '0'), vat = pf(fi.vat.val()) * 0.01; - if (qv > 0 && nv > 0) { - fi.net_val.val(rtp(qv * nv)); - fi.vat_val.val(rtp(qv * nv * vat)); - if (['Service'].includes(fi.Type.val())) { - fi.svcnet_val.val(rtp(qv * nv)); - fi.svcvat_val.val(rtp(qv * nv * vat)); - } - } -}; +/* No client-side arithmetic (ADR 0006/0008): quantity/price/VAT edits are posted as raw values + and the server (InvoiceDraftCalculator.RecomputeLineValues) computes net_val/vat_val/ + svcnet_val/svcvat_val. The fields are left as entered here; the authoritative values are + pushed back onto the row by $inv.d.applyItems once the change reaches the backend. */ +$inv.quantChange = function (i) { }; $inv.storno = function (id, fds) { let o, fr = $$.dc('choicefrm').append([ $$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); }) @@ -1203,6 +1658,8 @@ $inv.eRowR = function (ev) { $.extend(tdta.rm, res); tbl.data(tdta); $inv.rRemRw.call(row, tdta); + /* backend-authoritative: mirror the edited amount / amount-paid to the server session */ + $inv.rd.syncAmount(tdta.rm.amount, tdta.rm.amount_payed); }, typedvalues: true }); }; @@ -1217,7 +1674,10 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation //console.debug(response); let rem = response.rm || {}; let rq = $$.dc('srq', frm); - $ict.remt[rem.type].forEach(r => $$[0]('p').rwText(r).appendTo(rq)); + /* Intro/closing notice paragraphs are backend-authoritative (see $inv.rd.notes, + populated from FuchsPdf.BuildReminderNotes via rem/dstate after seeding below) — + they are appended/prepended into this same .srq container once the draft session + is open, so no client-side wording is constructed here. */ let rif = $$.tblset({ class: 'invi' }, rq); rif.ft = $$[0]('tfoot'); rif.tbl.data($.extend({ invid: rem.invid, new: {} }, response)); @@ -1225,7 +1685,6 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation $ict.remHR.forEach(h => $$.th(thr, h)); $inv.rRemRw.call($$.tr(rif.bdy), rif.tbl.data()); rif.ft.appendTo(rif.tbl); - $ict.remt2[rem.type].forEach(r => $$[0]('p').rwText(r).appendTo(rq)); let fm = (cls, txt, nme, hd, dta) => { let fr = $$.dc('inpfrm', frm).aC(cls).append(typeof hd === 'string' ? $$.dc('ahd', hd) : (hd > 0 ? $$.dc('ahd', $rct.frm[nme]) : null)), fc = $$.dc('content', fr).rwText(txt); @@ -1243,57 +1702,26 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation rif.tbl.children('tbody').each($inv.bdysort); rif.tbl.trigger('fds.inv'); /* trigger calculations */ + /* Seed the authoritative server session (ADR 0006). Amounts join the recipient + fields in the 'new' block; the reference invoice data goes into 'rem'. From here + the backend owns the open-amount computation and validation; inline edits and the + item-row dialog post single deltas (see $inv.rd). */ + let nw = rif.tbl.data('new'); + nw.amount = rem.amount; nw.amount_payed = rem.amount_payed; + $inv.rd.seed({ + rem: { invid: rem.invid, type: rem.type, invoiceid: rem.invoiceid, invoicedate: rem.invoicedate }, + new: nw + }); }, complete: () => { //o.c.trigger('modal_close'); } }); }; $inv.rprev = () => { - var l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(); - $.extend(d.new, tbl.find('tbody > tr:first').data()); - l.aC('freeze'); - //console.debug({ rem: d.rm, new: d.new }); - if ($fis.ValidateEmail(d.new.invoiceemail || '') === false) { - if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { - l.rC('freeze'); - return; - } - } - $ocms.postXT({ - url: $ocms.url('rem/prep'), data: { remc: JSON.stringify({ rem: d.rm, new: d.new }), id: d.invid || '' }, success: (response) => { - l.rC('freeze'); - let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), remid = response.id; - $.each(response.img || [], function (ii, img) { - $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); - }); - $ocms.dlg(c, { - size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $ict.remd, confirm: function (e) { - let ct = $(this); - $ocms.postXT({ - url: $ocms.url('rem/conf'), data: { id: remid }, success: () => { - ct.trigger('modal_close'); - window.open($ocms.url('rem/idoc') + '?id=' + remid, '_blank'); /* open pdf in new tab */ - $ocms.init('req'); /* go back to request list */ - $inv.rReload(); - }, error: () => { - alert($t.f1); - ct.trigger('modal_close'); - } - }); - }, cancel: function (e) { - let ct = $(this); - if (confirm($ict.cdI)) { - $ocms.postXT({ - url: $ocms.url('rem/del'), data: { - id: remid - } - }); - } - $inv.rReload(); - } - }); - } - }); + /* Preview + finalise now run through the backend-authoritative session ($inv.rd): + the PDF renders straight from the server cache (no rem/prep DB write), and confirm + flushes (rem/dsave) then finalises + emails (rem/conf). */ + $inv.rd.preview(); }; $inv.sis = (id) => { if (confirm($ict.sisc)) { diff --git a/Fuchs/js/intranet/modules/fis.inv_shared.scss b/Fuchs/js/intranet/modules/fis.inv_shared.scss index 707d506..9809a92 100644 --- a/Fuchs/js/intranet/modules/fis.inv_shared.scss +++ b/Fuchs/js/intranet/modules/fis.inv_shared.scss @@ -62,6 +62,18 @@ height: auto; cursor: pointer; + .content { + color: #000; + } + + .content:empty::before { + content: '-'; + min-width: 5rem; + height: 1rem; + display: block; + position: absolute; + } + &:hover { div.axf { display: block; @@ -232,6 +244,12 @@ border-color: #FFF; border-top: 1.5px solid #000; } + + /* Set-pricing display (see InvoiceSetPricing / $inv.d.applySetDisplay): the set + header line is emphasised the same way the PDF renders it (bold title). */ + &.sethdr td.txt { + font-weight: bold; + } } > tfoot > tr { @@ -399,6 +417,33 @@ } } +/* Backend-authoritative draft editing (ADR 0006): server-side plausibility findings + rendered above the editor, and the change-history dialog table. */ +.edit_frm .invoice_layout .dvalidation { + margin: 0 0 1rem 0; + + &.hidden { display: none; } + + .dvmsg { + padding: 0.4rem 0.7rem; + margin: 0.25rem 0; + border-radius: 0.2rem; + font-size: 0.9rem; + border-left: 4px solid #999; + background: #f5f5f5; + + &.error { border-left-color: #c0392b; background: #fdecea; color: #922; } + &.warning { border-left-color: #e0a800; background: #fff8e1; color: #7a5c00; } + &.info { border-left-color: #3498db; background: #eaf4fb; color: #1c5a82; } + } +} + +.dhist { + .invtbl { width: 100%; } + td, th { padding: 0.3rem 0.6rem; text-align: left; vertical-align: top; } + tbody tr:nth-child(even) { background: #fafafa; } +} + .modal-body .lstfrm { display: block; position: relative; diff --git a/Fuchs/js/intranet/modules/fis.inv_txt_de.js b/Fuchs/js/intranet/modules/fis.inv_txt_de.js index 3f34986..583bf94 100644 --- a/Fuchs/js/intranet/modules/fis.inv_txt_de.js +++ b/Fuchs/js/intranet/modules/fis.inv_txt_de.js @@ -43,7 +43,6 @@ setm: 'Set-Preisanzeige', setmo: { setprice: 'Set mit Preis – Positionen ohne Preis', - itemprices: 'Positionen mit Preis – Set als Überschrift', setonly: 'Nur Set mit Preis – Positionen ausgeblendet' }, ctp: 'Ansprechpartner festlegen', @@ -63,16 +62,10 @@ rovlh: 'Übersicht der bisherigen Mahnungen', rovl: ['Betreff', 'Betrag', 'Betrag gezahlt', 'fertiggestellt am'], remHR: ['Rechnung', 'vom', 'Rechnungsbetrag', 'bereits bezahlt', 'noch offen'], - remt: { - f: ['Sehr geehrte Damen und Herren,', 'ein Mahnschreiben sollte kurz, freundlich und erfolgreich sein. Kurz ist es, freundlich sowieso; ob es auch erfolgreich ist, hängt von Ihnen ab.'] - , m: ['Sehr geehrte Damen und Herren,', 'nun müssen wir Sie noch einmal anschreiben.', 'Wahrscheinlich haben Sie triftige Gründe dafür, warum Sie die Zahlung unserer Forderung nicht vornehmen und auch nicht auf unsere Mahnung reagieren. Sollten wir darüber nicht einmal sprechen?', 'Bitte nehmen Sie umgehend in dieser Sache mit uns Kontakt auf.'] - , l: ['Sehr geehrte Damen und Herren,', 'Eine DRITTE MAHNUNG zu erhalten bereitet Ihnen bestimmt ebenso wenig Freude wie uns, sie zu verschicken. Leider haben wir auf unsere zweite Mahnung noch keine Antwort von Ihnen erhalten.", "Wir bitten Sie, den offenen Betrag innerhalb der nächsten 7 Werktage nach Erhalt dieses Schreibens zu begleichen. Nach Ablauf dieser Frist erfolgt keine weitere Mahnung mehr.', 'Sollte die Forderung bis dahin nicht beglichen sein, eröffnen wir das gerichtliche Mahnverfahren. Sollten Sie die Rechnung inzwischen beglichen haben, so betrachten Sie bitte dieses Schreiben als gegenstandslos.'] - }, - remt2: { - f: ['Wir bitten Sie, den noch offenen Rechnungsbetrag innerhalb einer Woche auf unser Konto zu überweisen.', 'Sollten Sie den Betrag bereits überwiesen haben, so bitten wir Sie, diese Zahlungserinnerung als gegenstandslos zu betrachten.'] - , m: ['Um Ihnen zusätzliche Kosten für weitere Mahnungen zu ersparen, bitten wir Sie nunmehr um die Überweisung des noch zu zahlenden Gesamtbetrages inklusive der ggf. bereits fälligen Mahnzinsen und Mahngebühren innerhalb von einer Woche.'] - , l: [] - }, payi: { account: 'Konto', name: 'Zahler', text: 'Verw.Zweck', InvoiceID: 'Rechnung', amount: 'Betrag', date: 'Datum', manual: 'Typ'} + // Reminder intro/closing notice texts are backend-authoritative (see FuchsPdf.BuildReminderNotes / + // $inv.rd.notes) — no client-side copies here. + translatePaymentTerm: (pt) => (pt || '').replace('wd', ' Werktagen').replace('d', ' Tagen').replace('wk', ' Wochen') || '10 Tagen' + , payi: { account: 'Konto', name: 'Zahler', text: 'Verw.Zweck', InvoiceID: 'Rechnung', amount: 'Betrag', date: 'Datum', manual: 'Typ'} }, $invcol = { datev: new fields_definition('Rechnung', 'Rechnungen', [ { name: 'Umsatz (ohne Soll/Haben-Kz)', label: 'Umsatz (ohne Soll/Haben-Kz)', type: 'string' }, diff --git a/Fuchs/js/intranet/modules/fis.req_txt_de.js b/Fuchs/js/intranet/modules/fis.req_txt_de.js index a207693..f52f98c 100644 --- a/Fuchs/js/intranet/modules/fis.req_txt_de.js +++ b/Fuchs/js/intranet/modules/fis.req_txt_de.js @@ -34,6 +34,9 @@ svcPart: 'Service-Anteil', vat: 'Umsatzsteuer', combP: 'Positionen zusammenfassen', + toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)', + toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?', + toSetMc: 'Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?', iSum: 'Zwischensumme', dtRel: 'Freigegeben am: ', dtCr: 'Erstellt am: ', diff --git a/Fuchs/js/intranet/oci_basic.js b/Fuchs/js/intranet/oci_basic.js index d826cd1..75dc871 100644 --- a/Fuchs/js/intranet/oci_basic.js +++ b/Fuchs/js/intranet/oci_basic.js @@ -577,7 +577,7 @@ $ocms.postXT = function (options) { if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) { ajo.contentType = false; /* needed for file uploads */ } - $.ajax(ajo); + return $.ajax(ajo); }; $ocms.cex_timer = function () { if (!$ocms.cexi) { @@ -660,16 +660,16 @@ $ocms.vbl_send = function (ev) { } else { val = null; } - } else if (type === 'number' && typedvalues === true ) { - let v; + } else if ((type === 'number' || dtf[0] === 'float' || dtf[0] === 'integer') && typedvalues === true) { + let v, raw = String(val).replace(',', '.'); if (dtf[0] === 'integer') { - v = parseInt(val); + v = parseInt(raw); } else { - v = parseFloat(val); + v = parseFloat(raw); } val = isNaN(v) ? val : v; } - if (req === true && ((val || '') === '' || (val.match(pattern) === null))) { + if (req === true && ((val === '' || val === null || typeof val === 'undefined') || (typeof val === 'string' && val.match(pattern) === null))) { if (cat === false && bool(checkvalidity, false) === true) { e.setCustomValidity(t.nza('ocms-nvnote', $ocms.t.inv || 'Invalid field')); } val = null; } else if (cat=== false && bool(checkvalidity, false) === true) { diff --git a/Fuchs/js/intranet/oci_main.js b/Fuchs/js/intranet/oci_main.js index 25c007a..daf2271 100644 --- a/Fuchs/js/intranet/oci_main.js +++ b/Fuchs/js/intranet/oci_main.js @@ -441,6 +441,8 @@ break; case 'number': f.pattern = ne(f.pattern, '[-+]?[0-9]*[.,]?[0-9]*'); + inp.attr('type', 'text'); + inp.attr('inputmode', 'decimal'); inp.attr('step', f.precision || 'any'); inp.attr('data-format', 'float'); break; diff --git a/Fuchs/js/intranet/oci_sortable.js b/Fuchs/js/intranet/oci_sortable.js index 9458116..e870687 100644 --- a/Fuchs/js/intranet/oci_sortable.js +++ b/Fuchs/js/intranet/oci_sortable.js @@ -63,7 +63,8 @@ if (!Element.prototype.closest) { this._dragging = false; this._dragHandleClass = this._options.dragHandleClass || ''; this._parentident = this._options.parentident || ''; - this._swapdone = typeof this._options.swapdone === "function" ? this._options._swapdone : null; + this._swapdone = typeof this._options.swapdone === "function" ? this._options.swapdone : null; + this._onend = typeof this._options.onend === "function" ? this._options.onend : null; this._container.setAttribute("data-is-sortable", 1); this._container.classList.add("sortable"); @@ -197,7 +198,15 @@ if (!Element.prototype.closest) { _onPress: function (e) { if (!e) { return; } function op(tgt) { - if (tgt && tgt.parentNode === this._container && (this._dragHandleClass === '' || e.target.className.indexOf(this._dragHandleClass) > -1) && tgt.className.indexOf("nosort") < 0) { + // Match the drag handle by real CSS class token, not substring. The old + // e.target.className.indexOf('ico') matched every Bootstrap "glyph[ico]n" icon, + // so clicking any icon button (edit/delete/set-price…) inside a sortable row + // started a zero-distance drag that swallowed the button's own click. Using + // closest('.') restricts dragging to the dedicated handle element (e.g. + // the ".ico" move button) and its descendants only. + var isHandle = this._dragHandleClass === '' || + (e.target.closest && e.target.closest('.' + this._dragHandleClass) != null); + if (tgt && tgt.parentNode === this._container && isHandle && tgt.className.indexOf("nosort") < 0) { e.preventDefault(); this._dragging = true; this._click = getPoint(e); @@ -213,10 +222,17 @@ if (!Element.prototype.closest) { } }, - // on item release/drop + // on item release/drop _onRelease: function (e) { + // Was THIS list mid-drag? (mouseup fires on every instance's window listener.) + var wasDragging = this._dragging === true && this._clickItem !== null; this._dragging = false; this._trashDragItem(); + // Fire a single "drag finished" callback so callers can commit the new order once, + // reliably, on drop — rather than relying on the per-hover _swapdone during the drag. + if (wasDragging && typeof this._onend === 'function') { + this._onend(); + } }, // on item drag/move diff --git a/Fuchs/wwwroot/web/fis.admin.css b/Fuchs/wwwroot/web/fis.admin.css new file mode 100644 index 0000000..f672fe6 --- /dev/null +++ b/Fuchs/wwwroot/web/fis.admin.css @@ -0,0 +1,256 @@ +/* basics */ +/* Color SCHEME */ +/* #8396bd */ +/* #e3e6e6 */ +/* #ffffff */ +/* #262626 */ +/* #262626 */ +/* #e3e6e6 */ +/* #FFC801 */ +/* #ffff4a */ +/* media breaks */ +/* other */ +/* mixin */ +/* basics */ +/* Color SCHEME */ +/* #1b4379 #0033b3 */ +/*neu: #56a532 ; alt: rgb(32,144,119);*/ +/* 10% sw */ +.admfrm { + padding: 1.5rem; +} +.admfrm .adm_sec { + font-size: 1.2rem; + font-weight: bold; + color: rgb(27, 67, 121); + margin: 1.5rem 0 0.75rem; + border-bottom: 2px solid rgb(27, 67, 121); + padding-bottom: 0.25rem; +} +.admfrm .adm_sec:first-child { + margin-top: 0; +} +.admfrm .adm_err { + padding: 1rem; + color: #b00020; + font-weight: bold; +} +.admfrm .adm_grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(22rem, 1fr)); + gap: 1rem; + align-items: start; +} +.admfrm .adm_card { + border: 1px solid #d5d5d5; + border-radius: 0.4rem; + background: #fff; + box-shadow: 0 1px 3px rgba(50, 50, 50, 0.12); + overflow: hidden; +} +.admfrm .adm_card.adm_card_wide { + grid-column: 1/-1; +} +.admfrm .adm_card_hd { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 0.85rem; + background: rgb(27, 67, 121); + color: #fff; + font-weight: bold; +} +.admfrm .adm_card_hd > span:first-child { + flex: 1 1 auto; +} +.admfrm .adm_card_bd { + padding: 0.6rem 0.85rem; +} +.admfrm .adm_card_ft { + padding: 0.5rem 0.85rem 0.75rem; + text-align: right; +} +.admfrm table.adm_kv { + border-collapse: collapse; + width: 100%; +} +.admfrm table.adm_kv td { + padding: 0.25rem 0.35rem; + border-bottom: 1px solid #eee; + vertical-align: top; + font-size: 0.92rem; +} +.admfrm table.adm_kv td.k { + color: #666; + white-space: nowrap; + width: 40%; +} +.admfrm table.adm_kv td.v { + word-break: break-word; +} +.admfrm table.adm_kv tr:last-child td { + border-bottom: none; +} +.admfrm .adm_probe { + margin-top: 0.6rem; + padding-top: 0.5rem; + border-top: 1px dashed #ddd; + font-size: 0.88rem; +} +.admfrm .adm_probe .adm_probe_msg { + font-weight: 600; +} +.admfrm .adm_probe .adm_probe_det { + color: #555; + word-break: break-word; + margin-top: 0.15rem; +} +.admfrm .adm_probe .adm_probe_dur { + color: #999; + margin-top: 0.15rem; + font-size: 0.8rem; +} +.admfrm .adm_probe .adm_probe_metrics { + margin-top: 0.35rem; +} +.admfrm .adm_probe .adm_probe_metrics .adm_metric { + display: flex; + justify-content: space-between; + gap: 0.5rem; + padding: 0.1rem 0; + border-bottom: 1px dotted #eee; +} +.admfrm .adm_probe .adm_probe_metrics .adm_metric:last-child { + border-bottom: none; +} +.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_k { + color: #555; + word-break: break-word; +} +.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_v { + font-weight: 600; + white-space: nowrap; +} +.admfrm { + /* startup checks list */ +} +.admfrm .adm_checks { + margin-top: 0.5rem; +} +.admfrm .adm_checks .adm_check { + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0; + border-bottom: 1px solid #eee; +} +.admfrm .adm_checks .adm_check:last-child { + border-bottom: none; +} +.admfrm .adm_checks .adm_check .adm_check_k { + color: #333; +} +.admfrm .adm_checks .adm_check .adm_check_v { + font-weight: bold; + font-size: 0.8rem; + padding: 0.05rem 0.5rem; + border-radius: 0.25rem; + white-space: nowrap; +} +.admfrm .adm_checks .adm_check .adm_check_ok { + background: #e2f3da; + color: #2f6d18; +} +.admfrm .adm_checks .adm_check .adm_check_fail { + background: #f3ded9; + color: #c0341d; +} +.admfrm .adm_checks .adm_check .adm_check_skipped { + background: #ececec; + color: #777; +} +.admfrm { + /* status pill */ +} +.admfrm .adm_pill { + display: inline-block; + padding: 0.1rem 0.55rem; + border-radius: 1rem; + font-size: 0.78rem; + font-weight: bold; + color: #fff; + background: #999; + white-space: nowrap; +} +.admfrm .adm_pill_ok { + background: rgb(86, 165, 50); +} +.admfrm .adm_pill_error { + background: #c0341d; +} +.admfrm .adm_pill_disabled { + background: #888; +} +.admfrm .adm_pill_unconfigured { + background: #d69100; +} +.admfrm .adm_pill_checking { + background: #2a72c4; +} +.admfrm { + /* boolean chips */ +} +.admfrm .adm_bool { + display: inline-block; + padding: 0 0.4rem; + border-radius: 0.25rem; + font-size: 0.8rem; + font-weight: bold; +} +.admfrm .adm_bool_y { + background: #e2f3da; + color: #2f6d18; +} +.admfrm .adm_bool_n { + background: #f3e0dd; + color: #9c2a17; +} +.admfrm .adm_env { + font-size: 0.8rem; + font-weight: normal; + padding: 0.1rem 0.5rem; + border-radius: 0.25rem; + background: rgba(255, 255, 255, 0.25); +} +.admfrm .adm_env.adm_env_test { + background: #13a143; +} +.admfrm .adm_btn { + cursor: pointer; + border-radius: 0.28rem; + padding: 0.2rem 0.6rem; + font-size: 0.85rem; + border: 1px solid #ababab; + background-color: #fff; + color: #333; +} +.admfrm .adm_btn.adm_btn_primary { + background-color: rgb(86, 165, 50); + border-color: rgb(69.68, 133.688372093, 40.511627907); + color: #fff; +} +.admfrm .adm_card_hd .adm_btn { + background-color: rgba(255, 255, 255, 0.15); + color: #fff; + border-color: rgba(255, 255, 255, 0.4); +} + +.adm_note { + margin-top: 0.75rem; + padding: 0.5rem 0.6rem; + background: #f3f6fb; + border-left: 3px solid rgb(27, 67, 121); + font-size: 0.85rem; + color: #444; +} \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.admin.de.js b/Fuchs/wwwroot/web/fis.admin.de.js new file mode 100644 index 0000000..03d4c85 --- /dev/null +++ b/Fuchs/wwwroot/web/fis.admin.de.js @@ -0,0 +1,330 @@ +let $adt = { + mdl: 'Administration' + , sub_info: 'System' + , sub_status: 'Status & Konnektivität' + , sub_tests: 'Tests' + // startup checks widget (non-refreshable) + , startup: 'Startup-Prüfungen' + , startup_notrun: 'Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).' + , startup_on: 'Host' + , startup_at: 'Ausgeführt (UTC)' + , chk_ok: 'OK' + , chk_fail: 'Fehler' + , chk_skipped: 'übersprungen' + , chk_KeyVault: 'Azure Key Vault' + , chk_Database: 'SQL-Datenbank' + , chk_MFR: 'MFR (ERP)' + , chk_PdfLicense: 'Spire.PDF-Lizenz' + , chk_Mailer: 'E-Mail (Startup-Probe)' + // system info + , host: 'Host / Server' + , machine: 'Hostname' + , os: 'Betriebssystem' + , framework: 'Framework' + , env: 'Umgebung' + , testdep: 'Test-Deployment' + , pid: 'Prozess-ID' + , uptime: 'Laufzeit' + , since: 'Start (UTC)' + , hours: 'Std.' + // status cards + , db: 'SQL-Datenbank' + , mail: 'E-Mail-Dienst' + , blob: 'Azure Blob Storage' + , mfr: 'MFR (ERP)' + , kv: 'Azure Key Vault' + // labels + , server: 'Server' + , catalog: 'Datenbank' + , login: 'Login' + , mailer: 'Mailer aktiv' + , baseurl: 'Base-URL' + , account: 'Account' + , serverid: 'Server-ID' + , token: 'Token hinterlegt' + , override: 'OverrideRecipient' + , overrideActive: 'Umleitung aktiv' + , enabled: 'Aktiviert' + , configured: 'Konfiguriert' + , invContainer: 'Rechnungs-Container' + , remContainer: 'Mahnungs-Container' + , host_l: 'Host' + , creds: 'Zugangsdaten hinterlegt' + , sync: 'Sync aktiv' + , vault: 'Vault-URI' + , appname: 'App-Präfix' + , seccount: 'Verwaltete Secrets' + , clientreg: 'Client registriert' + , duration: 'Dauer' + , checked: 'Geprüft' + , detail: 'Detail' + // status values + , st_ok: 'OK' + , st_error: 'Fehler' + , st_disabled: 'Deaktiviert' + , st_unconfigured: 'Nicht konfiguriert' + , st_checking: 'Prüfe…' + // actions + , refresh: 'Aktualisieren' + , refreshAll: 'Alle prüfen' + , yes: 'Ja' + , no: 'Nein' + // test email + , testmail: 'Test-E-Mail' + , testmail_btn: 'Test-E-Mail senden' + , tm_to: 'Empfänger' + , tm_subject: 'Betreff' + , tm_body: 'Nachricht' + , tm_hint: 'Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.' + , tm_sent: 'Test-E-Mail übergeben.' + , tm_failed: 'Test-E-Mail konnte nicht gesendet werden.' + , tm_default_subject: 'Fuchs Intranet – Test-E-Mail' + , tm_default_body: 'Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.' + , loadfail: 'Der Systemstatus konnte nicht geladen werden.' +}; + +let $adm = { + init2: function () { + // No external libraries needed — go straight to render. + $ocms.getScript([], function () { $adm.init3(); }); + }, + init3: function () { + $fis.cf(true); + $fis.lf(true); + $('#activemodule').text($adt.mdl); + $adm.topbar(); + let frm = $$.dc('admfrm', $('#contentframe')).ldng(1); + $adm.load(frm); + }, + topbar: function () { + return $('#topbar').ocmsmenu([ + { lbl: $adt.refreshAll, id: 'adm_refresh_all', ico: 'glyphicon glyphicon-refresh', fnc: function () { $adm.init3(); } } + ]); + }, + load: function (frm) { + $ocms.postXT({ + url: $ocms.url('admin/status'), success: function (r) { + frm.empty(); + $adm.render(frm, r.info || {}, r.probes || []); + }, error: function () { + frm.empty(); + $$.dc('adm_err', frm).text($adt.loadfail); + }, complete: function () { frm.ldng(0); } + }); + }, + render: function (frm, info, probes) { + let pmap = {}; + $.each(probes, function (i, p) { pmap[p.component] = p; }); + + $$.dc('adm_sec', frm).text($adt.sub_info); + $adm.sysCard(frm, info); + $adm.startupCard(frm, info.startupChecks); + + $$.dc('adm_sec', frm).text($adt.sub_status); + let grid = $$.dc('adm_grid', frm); + $adm.dbCard(grid, info.database || {}, pmap.database); + $adm.mailCard(grid, info.email || {}); + $adm.blobCard(grid, info.blob || {}, pmap.blob); + $adm.mfrCard(grid, info.mfr || {}, pmap.mfr); + $adm.kvCard(grid, info.keyVault || {}, pmap.keyvault); + }, + + /* ── generic building blocks ─────────────────────────────────────────── */ + kvTable: function (parent, rows) { + let t = $$.tbl().addClass('adm_kv'); + $.each(rows || [], function (i, r) { + if (!r) { return true; } + let tr = $$.tr(t); + $$.td(tr).addClass('k').text(r[0] == null ? '' : String(r[0])); + let v = $$.td(tr).addClass('v'); + if (r[1] instanceof jQuery) { v.append(r[1]); } else { v.text(r[1] == null ? '' : String(r[1])); } + }); + if (parent instanceof jQuery) { parent.append(t); } + return t; + }, + yesNo: function (b) { + return $$.sc('adm_bool adm_bool_' + (b ? 'y' : 'n'), b ? $adt.yes : $adt.no); + }, + setPill: function (pill, probe) { + probe = probe || {}; + let st = probe.status || 'unconfigured'; + let lbl = { + ok: $adt.st_ok, error: $adt.st_error, disabled: $adt.st_disabled, + unconfigured: $adt.st_unconfigured, checking: $adt.st_checking + }[st] || st; + pill.attr('class', 'adm_pill adm_pill_' + st).text(lbl); + return pill; + }, + + /* Connectivity card with a live status pill + per-card refresh button. */ + probeCard: function (grid, title, component, cfgRows, probe) { + let card = $$.dc('adm_card', grid); + let hd = $$.dc('adm_card_hd', card); + $$.s(title).appendTo(hd); + let pill = $$.sc('adm_pill').appendTo(hd); + let rbtn = $$.bbtn($adt.refresh, 'adm_btn').appendTo(hd); + let bd = $$.dc('adm_card_bd', card); + $adm.kvTable(bd, cfgRows); + + let pf = $$.dc('adm_probe', bd); + let msg = $$.dc('adm_probe_msg', pf); + let met = $$.dc('adm_probe_metrics', pf); + let det = $$.dc('adm_probe_det', pf); + let dur = $$.dc('adm_probe_dur', pf); + let apply = function (p) { + p = p || {}; + $adm.setPill(pill, p); + msg.text(p.message || ''); + met.empty(); + $.each(p.metrics || [], function (i, m) { + let row = $$.dc('adm_metric', met); + $$.sc('adm_metric_k').text(m.label || '').appendTo(row); + $$.sc('adm_metric_v').text(m.value == null ? '' : String(m.value)).appendTo(row); + }); + det.text(p.detail || ''); + dur.text(p.durationMs != null ? (p.durationMs + ' ms') : ''); + }; + apply(probe); + + rbtn.click(function () { + $adm.setPill(pill, { status: 'checking' }); + msg.text(''); met.empty(); det.text(''); dur.text(''); + $ocms.postXT({ + url: $ocms.url('admin/probe/' + component), success: function (r) { + apply(r.probe); + }, error: function () { + $adm.setPill(pill, { status: 'error' }); + msg.text($adt.st_error); + } + }); + }); + return card; + }, + + /* ── individual cards ────────────────────────────────────────────────── */ + sysCard: function (frm, info) { + let card = $$.dc('adm_card adm_card_wide', frm); + let hd = $$.dc('adm_card_hd', card); + $$.s($adt.host).appendTo(hd); + let env = $$.sc('adm_env').text(info.environment || ''); + if (info.isTestDeployment === true) { env.aC('adm_env_test'); } + env.appendTo(hd); + let bd = $$.dc('adm_card_bd', card); + $adm.kvTable(bd, [ + [$adt.machine, info.machineName], + [$adt.os, info.osDescription], + [$adt.framework, info.frameworkDescription], + [$adt.env, info.environment], + [$adt.testdep, $adm.yesNo(info.isTestDeployment === true)], + [$adt.pid, info.processId], + [$adt.since, info.processStartUtc ? fdt(info.processStartUtc) : ''], + [$adt.uptime, (info.uptimeHours != null ? (info.uptimeHours + ' ' + $adt.hours) : '')] + ]); + }, + /* Non-refreshable widget: the one-shot startup self-test result captured at boot. */ + startupCard: function (frm, sc) { + let card = $$.dc('adm_card adm_card_wide', frm); + let hd = $$.dc('adm_card_hd', card); + $$.s($adt.startup).appendTo(hd); + let bd = $$.dc('adm_card_bd', card); + + if (!sc || sc.ran !== true) { + $adm.setPill($$.sc('adm_pill').appendTo(hd), { status: 'disabled' }); + $$.dc('adm_note', bd).text($adt.startup_notrun); + return; + } + + let items = sc.items || []; + let anyFail = false; + $.each(items, function (i, it) { if (it.enabled === true && it.ok !== true) { anyFail = true; } }); + $adm.setPill($$.sc('adm_pill').appendTo(hd), { status: anyFail ? 'error' : 'ok' }); + + $adm.kvTable(bd, [ + [$adt.startup_on, sc.machineName], + [$adt.startup_at, sc.completedUtc ? fdt(sc.completedUtc) : ''] + ]); + + let list = $$.dc('adm_checks', bd); + $.each(items, function (i, it) { + let row = $$.dc('adm_check', list); + $$.sc('adm_check_k').text($adt['chk_' + it.name] || it.name).appendTo(row); + let stt = it.enabled !== true ? 'skipped' : (it.ok === true ? 'ok' : 'fail'); + let lbl = { ok: $adt.chk_ok, fail: $adt.chk_fail, skipped: $adt.chk_skipped }[stt]; + $$.sc('adm_check_v adm_check_' + stt).text(lbl).appendTo(row); + }); + }, + dbCard: function (grid, d, probe) { + $adm.probeCard(grid, $adt.db, 'database', [ + [$adt.configured, $adm.yesNo(d.configured === true)], + [$adt.server, d.server], + [$adt.catalog, d.catalog], + [$adt.login, d.userId] + ], probe); + }, + mailCard: function (grid, e) { + let card = $$.dc('adm_card', grid); + let hd = $$.dc('adm_card_hd', card); + $$.s($adt.mail).appendTo(hd); + $adm.setPill($$.sc('adm_pill').appendTo(hd), { status: e.mailerEnabled === true ? 'ok' : 'disabled' }); + let bd = $$.dc('adm_card_bd', card); + let rows = [ + [$adt.mailer, $adm.yesNo(e.mailerEnabled === true)], + [$adt.baseurl, e.baseUrl], + [$adt.account, e.accountId], + [$adt.serverid, e.serverId], + [$adt.token, $adm.yesNo(e.tokenConfigured === true)], + [$adt.overrideActive, $adm.yesNo(e.overrideActive === true)] + ]; + if (e.overrideActive === true) { rows.push([$adt.override, e.overrideRecipient]); } + $adm.kvTable(bd, rows); + let ft = $$.dc('adm_card_ft', card); + $$.bbtn($adt.testmail_btn, 'adm_btn adm_btn_primary').appendTo(ft).click($adm.testMailDlg); + }, + blobCard: function (grid, b, probe) { + $adm.probeCard(grid, $adt.blob, 'blob', [ + [$adt.enabled, $adm.yesNo(b.enabled === true)], + [$adt.configured, $adm.yesNo(b.configured === true)], + [$adt.invContainer, b.invoiceContainer], + [$adt.remContainer, b.reminderContainer] + ], probe); + }, + mfrCard: function (grid, m, probe) { + $adm.probeCard(grid, $adt.mfr, 'mfr', [ + [$adt.host_l, m.host], + [$adt.creds, $adm.yesNo(m.credentialsConfigured === true)], + [$adt.sync, $adm.yesNo(m.syncEnabled === true)] + ], probe); + }, + kvCard: function (grid, k, probe) { + $adm.probeCard(grid, $adt.kv, 'keyvault', [ + [$adt.vault, k.vaultUri], + [$adt.appname, k.appName], + [$adt.seccount, k.managedSecretCount], + [$adt.clientreg, $adm.yesNo(k.clientRegistered === true)] + ], probe); + }, + + /* ── test email ──────────────────────────────────────────────────────── */ + testMailDlg: function () { + $ocms.dlgform([ + { name: 'to', label: $adt.tm_to, type: 'email', required: true, value: ($ocms.auth.email || '') }, + { name: 'subject', label: $adt.tm_subject, type: 'string', required: true, value: $adt.tm_default_subject }, + { name: 'body', label: $adt.tm_body, type: 'text', required: true, value: $adt.tm_default_body } + ], { + title: $adt.testmail, + button: $adt.testmail_btn, + size: [420, 560], + addcontent: $$.dc('adm_note').text($adt.tm_hint), + url: $ocms.url('admin/testmail'), + typedvalues: true, + /* Runs inside jQuery's success dispatch — it must never throw, or postXT's + `complete` (which clears the page loading indicator) is skipped. */ + success: function (r) { + let res = (r && r.result) || {}; + alert(res.message || (res.sent === true ? $adt.tm_sent : $adt.tm_failed)); + } + }); + } +}; +let $$adm = { init2: $adm.init2, auth: {} }; +export default $$adm; diff --git a/Fuchs/wwwroot/web/fis.admin.de.min.js b/Fuchs/wwwroot/web/fis.admin.de.min.js new file mode 100644 index 0000000..9219582 --- /dev/null +++ b/Fuchs/wwwroot/web/fis.admin.de.min.js @@ -0,0 +1 @@ +let $adt={mdl:"Administration",sub_info:"System",sub_status:"Status & Konnektivität",sub_tests:"Tests",startup:"Startup-Prüfungen",startup_notrun:"Die Startup-Prüfungen sind deaktiviert oder wurden noch nicht ausgeführt (Fuchs:StartupChecks:Enabled).",startup_on:"Host",startup_at:"Ausgeführt (UTC)",chk_ok:"OK",chk_fail:"Fehler",chk_skipped:"übersprungen",chk_KeyVault:"Azure Key Vault",chk_Database:"SQL-Datenbank",chk_MFR:"MFR (ERP)",chk_PdfLicense:"Spire.PDF-Lizenz",chk_Mailer:"E-Mail (Startup-Probe)",host:"Host / Server",machine:"Hostname",os:"Betriebssystem",framework:"Framework",env:"Umgebung",testdep:"Test-Deployment",pid:"Prozess-ID",uptime:"Laufzeit",since:"Start (UTC)",hours:"Std.",db:"SQL-Datenbank",mail:"E-Mail-Dienst",blob:"Azure Blob Storage",mfr:"MFR (ERP)",kv:"Azure Key Vault",server:"Server",catalog:"Datenbank",login:"Login",mailer:"Mailer aktiv",baseurl:"Base-URL",account:"Account",serverid:"Server-ID",token:"Token hinterlegt",override:"OverrideRecipient",overrideActive:"Umleitung aktiv",enabled:"Aktiviert",configured:"Konfiguriert",invContainer:"Rechnungs-Container",remContainer:"Mahnungs-Container",host_l:"Host",creds:"Zugangsdaten hinterlegt",sync:"Sync aktiv",vault:"Vault-URI",appname:"App-Präfix",seccount:"Verwaltete Secrets",clientreg:"Client registriert",duration:"Dauer",checked:"Geprüft",detail:"Detail",st_ok:"OK",st_error:"Fehler",st_disabled:"Deaktiviert",st_unconfigured:"Nicht konfiguriert",st_checking:"Prüfe…",refresh:"Aktualisieren",refreshAll:"Alle prüfen",yes:"Ja",no:"Nein",testmail:"Test-E-Mail",testmail_btn:"Test-E-Mail senden",tm_to:"Empfänger",tm_subject:"Betreff",tm_body:"Nachricht",tm_hint:"Der Versand nutzt die normale E-Mail-Pipeline; bei gesetztem OverrideRecipient wird die Nachricht dorthin umgeleitet.",tm_sent:"Test-E-Mail übergeben.",tm_failed:"Test-E-Mail konnte nicht gesendet werden.",tm_default_subject:"Fuchs Intranet – Test-E-Mail",tm_default_body:"Dies ist eine Test-E-Mail aus dem Administrationsbereich des Fuchs Intranet.",loadfail:"Der Systemstatus konnte nicht geladen werden."},$adm={init2:function(){$ocms.getScript([],(function(){$adm.init3()}))},init3:function(){$fis.cf(!0),$fis.lf(!0),$("#activemodule").text($adt.mdl),$adm.topbar();let e=$$.dc("admfrm",$("#contentframe")).ldng(1);$adm.load(e)},topbar:function(){return $("#topbar").ocmsmenu([{lbl:$adt.refreshAll,id:"adm_refresh_all",ico:"glyphicon glyphicon-refresh",fnc:function(){$adm.init3()}}])},load:function(e){$ocms.postXT({url:$ocms.url("admin/status"),success:function(t){e.empty(),$adm.render(e,t.info||{},t.probes||[])},error:function(){e.empty(),$$.dc("adm_err",e).text($adt.loadfail)},complete:function(){e.ldng(0)}})},render:function(e,t,a){let d={};$.each(a,(function(e,t){d[t.component]=t})),$$.dc("adm_sec",e).text($adt.sub_info),$adm.sysCard(e,t),$adm.startupCard(e,t.startupChecks),$$.dc("adm_sec",e).text($adt.sub_status);let r=$$.dc("adm_grid",e);$adm.dbCard(r,t.database||{},d.database),$adm.mailCard(r,t.email||{}),$adm.blobCard(r,t.blob||{},d.blob),$adm.mfrCard(r,t.mfr||{},d.mfr),$adm.kvCard(r,t.keyVault||{},d.keyvault)},kvTable:function(e,t){let a=$$.tbl().addClass("adm_kv");return $.each(t||[],(function(e,t){if(!t)return!0;let d=$$.tr(a);$$.td(d).addClass("k").text(null==t[0]?"":String(t[0]));let r=$$.td(d).addClass("v");t[1]instanceof jQuery?r.append(t[1]):r.text(null==t[1]?"":String(t[1]))})),e instanceof jQuery&&e.append(a),a},yesNo:function(e){return $$.sc("adm_bool adm_bool_"+(e?"y":"n"),e?$adt.yes:$adt.no)},setPill:function(e,t){let a=(t=t||{}).status||"unconfigured",d={ok:$adt.st_ok,error:$adt.st_error,disabled:$adt.st_disabled,unconfigured:$adt.st_unconfigured,checking:$adt.st_checking}[a]||a;return e.attr("class","adm_pill adm_pill_"+a).text(d),e},probeCard:function(e,t,a,d,r){let n=$$.dc("adm_card",e),i=$$.dc("adm_card_hd",n);$$.s(t).appendTo(i);let s=$$.sc("adm_pill").appendTo(i),o=$$.bbtn($adt.refresh,"adm_btn").appendTo(i),c=$$.dc("adm_card_bd",n);$adm.kvTable(c,d);let l=$$.dc("adm_probe",c),m=$$.dc("adm_probe_msg",l),u=$$.dc("adm_probe_metrics",l),_=$$.dc("adm_probe_det",l),p=$$.dc("adm_probe_dur",l),b=function(e){e=e||{},$adm.setPill(s,e),m.text(e.message||""),u.empty(),$.each(e.metrics||[],(function(e,t){let a=$$.dc("adm_metric",u);$$.sc("adm_metric_k").text(t.label||"").appendTo(a),$$.sc("adm_metric_v").text(null==t.value?"":String(t.value)).appendTo(a)})),_.text(e.detail||""),p.text(null!=e.durationMs?e.durationMs+" ms":"")};return b(r),o.click((function(){$adm.setPill(s,{status:"checking"}),m.text(""),u.empty(),_.text(""),p.text(""),$ocms.postXT({url:$ocms.url("admin/probe/"+a),success:function(e){b(e.probe)},error:function(){$adm.setPill(s,{status:"error"}),m.text($adt.st_error)}})})),n},sysCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.host).appendTo(d);let r=$$.sc("adm_env").text(t.environment||"");!0===t.isTestDeployment&&r.aC("adm_env_test"),r.appendTo(d);let n=$$.dc("adm_card_bd",a);$adm.kvTable(n,[[$adt.machine,t.machineName],[$adt.os,t.osDescription],[$adt.framework,t.frameworkDescription],[$adt.env,t.environment],[$adt.testdep,$adm.yesNo(!0===t.isTestDeployment)],[$adt.pid,t.processId],[$adt.since,t.processStartUtc?fdt(t.processStartUtc):""],[$adt.uptime,null!=t.uptimeHours?t.uptimeHours+" "+$adt.hours:""]])},startupCard:function(e,t){let a=$$.dc("adm_card adm_card_wide",e),d=$$.dc("adm_card_hd",a);$$.s($adt.startup).appendTo(d);let r=$$.dc("adm_card_bd",a);if(!t||!0!==t.ran)return $adm.setPill($$.sc("adm_pill").appendTo(d),{status:"disabled"}),void $$.dc("adm_note",r).text($adt.startup_notrun);let n=t.items||[],i=!1;$.each(n,(function(e,t){!0===t.enabled&&!0!==t.ok&&(i=!0)})),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:i?"error":"ok"}),$adm.kvTable(r,[[$adt.startup_on,t.machineName],[$adt.startup_at,t.completedUtc?fdt(t.completedUtc):""]]);let s=$$.dc("adm_checks",r);$.each(n,(function(e,t){let a=$$.dc("adm_check",s);$$.sc("adm_check_k").text($adt["chk_"+t.name]||t.name).appendTo(a);let d=!0!==t.enabled?"skipped":!0===t.ok?"ok":"fail",r={ok:$adt.chk_ok,fail:$adt.chk_fail,skipped:$adt.chk_skipped}[d];$$.sc("adm_check_v adm_check_"+d).text(r).appendTo(a)}))},dbCard:function(e,t,a){$adm.probeCard(e,$adt.db,"database",[[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.server,t.server],[$adt.catalog,t.catalog],[$adt.login,t.userId]],a)},mailCard:function(e,t){let a=$$.dc("adm_card",e),d=$$.dc("adm_card_hd",a);$$.s($adt.mail).appendTo(d),$adm.setPill($$.sc("adm_pill").appendTo(d),{status:!0===t.mailerEnabled?"ok":"disabled"});let r=$$.dc("adm_card_bd",a),n=[[$adt.mailer,$adm.yesNo(!0===t.mailerEnabled)],[$adt.baseurl,t.baseUrl],[$adt.account,t.accountId],[$adt.serverid,t.serverId],[$adt.token,$adm.yesNo(!0===t.tokenConfigured)],[$adt.overrideActive,$adm.yesNo(!0===t.overrideActive)]];!0===t.overrideActive&&n.push([$adt.override,t.overrideRecipient]),$adm.kvTable(r,n);let i=$$.dc("adm_card_ft",a);$$.bbtn($adt.testmail_btn,"adm_btn adm_btn_primary").appendTo(i).click($adm.testMailDlg)},blobCard:function(e,t,a){$adm.probeCard(e,$adt.blob,"blob",[[$adt.enabled,$adm.yesNo(!0===t.enabled)],[$adt.configured,$adm.yesNo(!0===t.configured)],[$adt.invContainer,t.invoiceContainer],[$adt.remContainer,t.reminderContainer]],a)},mfrCard:function(e,t,a){$adm.probeCard(e,$adt.mfr,"mfr",[[$adt.host_l,t.host],[$adt.creds,$adm.yesNo(!0===t.credentialsConfigured)],[$adt.sync,$adm.yesNo(!0===t.syncEnabled)]],a)},kvCard:function(e,t,a){$adm.probeCard(e,$adt.kv,"keyvault",[[$adt.vault,t.vaultUri],[$adt.appname,t.appName],[$adt.seccount,t.managedSecretCount],[$adt.clientreg,$adm.yesNo(!0===t.clientRegistered)]],a)},testMailDlg:function(){$ocms.dlgform([{name:"to",label:$adt.tm_to,type:"email",required:!0,value:$ocms.auth.email||""},{name:"subject",label:$adt.tm_subject,type:"string",required:!0,value:$adt.tm_default_subject},{name:"body",label:$adt.tm_body,type:"text",required:!0,value:$adt.tm_default_body}],{title:$adt.testmail,button:$adt.testmail_btn,size:[420,560],addcontent:$$.dc("adm_note").text($adt.tm_hint),url:$ocms.url("admin/testmail"),typedvalues:!0,success:function(e){let t=e&&e.result||{};alert(t.message||(!0===t.sent?$adt.tm_sent:$adt.tm_failed))}})}},$$adm={init2:$adm.init2,auth:{}};export default $$adm; \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.admin.min.css b/Fuchs/wwwroot/web/fis.admin.min.css new file mode 100644 index 0000000..ba7f0c9 --- /dev/null +++ b/Fuchs/wwwroot/web/fis.admin.min.css @@ -0,0 +1 @@ +.admfrm{padding:1.5rem}.admfrm .adm_sec{font-size:1.2rem;font-weight:700;color:#1b4379;margin:1.5rem 0 .75rem;border-bottom:2px solid #1b4379;padding-bottom:.25rem}.admfrm .adm_sec:first-child{margin-top:0}.admfrm .adm_err{padding:1rem;color:#b00020;font-weight:700}.admfrm .adm_grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(22rem,1fr));gap:1rem;align-items:start}.admfrm .adm_card{border:1px solid #d5d5d5;border-radius:.4rem;background:#fff;box-shadow:0 1px 3px rgba(50,50,50,.12);overflow:hidden}.admfrm .adm_card.adm_card_wide{grid-column:1/-1}.admfrm .adm_card_hd{display:flex;align-items:center;gap:.5rem;padding:.6rem .85rem;background:#1b4379;color:#fff;font-weight:700}.admfrm .adm_card_hd>span:first-child{flex:1 1 auto}.admfrm .adm_card_bd{padding:.6rem .85rem}.admfrm .adm_card_ft{padding:.5rem .85rem .75rem;text-align:right}.admfrm table.adm_kv{border-collapse:collapse;width:100%}.admfrm table.adm_kv td{padding:.25rem .35rem;border-bottom:1px solid #eee;vertical-align:top;font-size:.92rem}.admfrm table.adm_kv td.k{color:#666;white-space:nowrap;width:40%}.admfrm table.adm_kv td.v{word-break:break-word}.admfrm table.adm_kv tr:last-child td{border-bottom:none}.admfrm .adm_probe{margin-top:.6rem;padding-top:.5rem;border-top:1px dashed #ddd;font-size:.88rem}.admfrm .adm_probe .adm_probe_msg{font-weight:600}.admfrm .adm_probe .adm_probe_det{color:#555;word-break:break-word;margin-top:.15rem}.admfrm .adm_probe .adm_probe_dur{color:#999;margin-top:.15rem;font-size:.8rem}.admfrm .adm_probe .adm_probe_metrics{margin-top:.35rem}.admfrm .adm_probe .adm_probe_metrics .adm_metric{display:flex;justify-content:space-between;gap:.5rem;padding:.1rem 0;border-bottom:1px dotted #eee}.admfrm .adm_probe .adm_probe_metrics .adm_metric:last-child{border-bottom:none}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_k{color:#555;word-break:break-word}.admfrm .adm_probe .adm_probe_metrics .adm_metric .adm_metric_v{font-weight:600;white-space:nowrap}.admfrm .adm_checks{margin-top:.5rem}.admfrm .adm_checks .adm_check{display:flex;justify-content:space-between;align-items:center;gap:.5rem;padding:.25rem 0;border-bottom:1px solid #eee}.admfrm .adm_checks .adm_check:last-child{border-bottom:none}.admfrm .adm_checks .adm_check .adm_check_k{color:#333}.admfrm .adm_checks .adm_check .adm_check_v{font-weight:700;font-size:.8rem;padding:.05rem .5rem;border-radius:.25rem;white-space:nowrap}.admfrm .adm_checks .adm_check .adm_check_ok{background:#e2f3da;color:#2f6d18}.admfrm .adm_checks .adm_check .adm_check_fail{background:#f3ded9;color:#c0341d}.admfrm .adm_checks .adm_check .adm_check_skipped{background:#ececec;color:#777}.admfrm .adm_pill{display:inline-block;padding:.1rem .55rem;border-radius:1rem;font-size:.78rem;font-weight:700;color:#fff;background:#999;white-space:nowrap}.admfrm .adm_pill_ok{background:#56a532}.admfrm .adm_pill_error{background:#c0341d}.admfrm .adm_pill_disabled{background:#888}.admfrm .adm_pill_unconfigured{background:#d69100}.admfrm .adm_pill_checking{background:#2a72c4}.admfrm .adm_bool{display:inline-block;padding:0 .4rem;border-radius:.25rem;font-size:.8rem;font-weight:700}.admfrm .adm_bool_y{background:#e2f3da;color:#2f6d18}.admfrm .adm_bool_n{background:#f3e0dd;color:#9c2a17}.admfrm .adm_env{font-size:.8rem;font-weight:400;padding:.1rem .5rem;border-radius:.25rem;background:rgba(255,255,255,.25)}.admfrm .adm_env.adm_env_test{background:#13a143}.admfrm .adm_btn{cursor:pointer;border-radius:.28rem;padding:.2rem .6rem;font-size:.85rem;border:1px solid #ababab;background-color:#fff;color:#333}.admfrm .adm_btn.adm_btn_primary{background-color:#56a532;border-color:rgb(69.68,133.688372093,40.511627907);color:#fff}.admfrm .adm_card_hd .adm_btn{background-color:rgba(255,255,255,.15);color:#fff;border-color:rgba(255,255,255,.4)}.adm_note{margin-top:.75rem;padding:.5rem .6rem;background:#f3f6fb;border-left:3px solid #1b4379;font-size:.85rem;color:#444} \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.bam.de.js b/Fuchs/wwwroot/web/fis.bam.de.js index 23b8969..bed80c5 100644 --- a/Fuchs/wwwroot/web/fis.bam.de.js +++ b/Fuchs/wwwroot/web/fis.bam.de.js @@ -18,10 +18,21 @@ let $bct = { }, in: 'Rechnungsnummer', bt: 'Buchungstexte', - bv: 'Buchungswert (volle €)' + bv: 'Buchungswert (volle €)', + mbe: 'Manuelle Buchung erfassen', + mbc: 'Manuelle Buchung ändern', + mbd: 'Manuelle Buchung löschen', + mbdc: 'Manuelle Buchung wirklich löschen?\nDieser Vorgang kann nicht rückgängig gemacht werden.' }; let $bcol = { + man: new fields_definition('Manuelle Buchung', 'Manuelle Buchungen', [ + { name: 'taID', type: 'hidden' }, + { name: 'ValueDate', label: 'Valuta', type: 'date', required: true }, + { name: 'Amount', label: 'Betrag', type: 'number', precision: '0.01', required: true }, + { name: 'NameOfPayer', label: 'Name', type: 'string' }, + { name: 'SepaRemittanceInformation', label: 'Verwendungszweck', type: 'string' } + ]), qtl: new fields_definition('Kontobericht', 'Kontoberichte', [ { name: 'InvoiceId', label: 'RechnungsNr', type: 'string' }, { name: 'ValueDate', label: 'Valuta', type: 'date' }, @@ -77,11 +88,16 @@ let $bam = { let fds = bool(ix.fds, false), id = ix.taID, m = []; m.push({ lbl: $bct.smd, fnc: () => { $bam.smd(id); } }); m.push({ lbl: $bct.ati, fnc: () => { $bam.ati(id); } }); + if (bool(ix.IsManual, false) === true) { + m.push({ lbl: $bct.mbc, fnc: () => { $bam.mbc(id); } }); + m.push({ lbl: $bct.mbd, fnc: () => { $bam.mbd(id); } }); + } return $('#topbar').ocmsmenu(m); }, eM : (r, re, opt) => { let m = []; if ($fis.isAuth('fds_bam', 2) === true) { m.push({ lbl: $bct.upl, fnc: $bam.ubs}); + m.push({ lbl: $bct.mbe, fnc: $bam.mbe}); } if (bool(r, false) === true) { m.push({ lbl: $bct.rel, fnc: $bam.renderLst }); @@ -353,6 +369,32 @@ let $bam = { } } }); + }, mbe: () => { + $bam.manForm({}); + }, mbc: (taid) => { + $ocms.postXT({ + url: $ocms.url('bam/mget'), data: { taid: taid }, success: (dta) => { + $bam.manForm(dta || {}); + } + }); + }, mbd: (taid) => { + if (confirm($bct.mbdc)) { + $ocms.postXT({ + url: $ocms.url('bam/mdel'), data: { taid: taid }, success: () => { + $bam.renderLst(); + } + }); + } + }, manForm: (dta) => { + let flds = $bcol.man.clone().applyValues(dta || {}); + $ocms.dlgform(flds, { + title: (ne(dta.taID, '') !== '') ? $bct.mbc : $bct.mbe, + url: $ocms.url('bam/man'), + typedvalues: true, + success: () => { + $bam.renderLst(); + } + }); } } diff --git a/Fuchs/wwwroot/web/fis.bam.de.min.js b/Fuchs/wwwroot/web/fis.bam.de.min.js index 7be85c1..d4f518e 100644 --- a/Fuchs/wwwroot/web/fis.bam.de.min.js +++ b/Fuchs/wwwroot/web/fis.bam.de.min.js @@ -1 +1 @@ -let $bct={wk:"Woche",nd:"Keine Daten gefunden.",h:"Uhr",mdl:"BankBuchungen",upl:"BuchungsExport hochladen",rel:"Neu Laden",smd:"Buchung als erledigt markieren",smdc:"Buchung wirklich als erledigt markieren?\nDie Buchung wird nicht mehr in den auffälligen Buchungen angezeigt.",ati:"Buchung einer Rechnung zuordnen",ni:"Es wurden keine Rechnungen mit dieser ID gefunden.",ino:"Rechnungsnummer",fi:"Folgende Rechnungen wurden gefunden",atic:"Buchung wirklich der Rechnung {0} zuordnen?",dqb:"Auffällige Zahlungen anzeigen",iov:{all:"Buchungsübersicht (alle)","":"Buchungsübersicht"},in:"Rechnungsnummer",bt:"Buchungstexte",bv:"Buchungswert (volle €)"},$bcol={qtl:new fields_definition("Kontobericht","Kontoberichte",[{name:"InvoiceId",label:"RechnungsNr",type:"string"},{name:"ValueDate",label:"Valuta",type:"date"},{name:"InvoiceBalance",label:"Rechnungsbetrag",type:"string"},{name:"IsCanceled",label:"Storno",type:"bool"},{name:"Amount",label:"Gutschrift",type:"string"},{name:"Skonto",label:"Skonto ?",type:"bool"},{name:"Deviation",label:"Abweichung [%]",type:"bool"},{name:"AccountNumberOfPayer",label:"IBAN",type:"string"},{name:"NameOfPayer",label:"Name",type:"string"},{name:"SepaRemittanceInformation",label:"Verwendungszweck",type:"string"},{name:"EndToEndReference",label:"Referenz",type:"string"}]),bsu:new fields_definition("Kontobericht","Kontoberichte",[{name:"bsu",label:"Export der Buchungen",type:"file",required:!0,prop:{multiple:!0}}])},gi=(e,t)=>$$.sc(`glyphicon glyphicon-${e}`).aC(t),$bam={init2:function(e,t){e=e||"bam",t=t||{},$ocms.getScript([],(function(){$bam.init3(e,t)}))},init3:async function(e,t){$fis.cf(!0);let n=$fis.lf(!0);$bam.eM(),$("#activemodule").text($bct.mdl),await $fis.prepAuth("fds_bam,fds_inv,fds_reminder");let a=[(async()=>{!0===$fis.isAuth("fds_bam",1)&&($bam.prepLst(""),n.aC("fix"))})(),(async()=>{!0===$fis.isAuth("fds_bam",1)&&$bam.renderLst(!1)})(),new Promise(((e,t)=>{}))];await Promise.all(a)},iMn:e=>{bool(e.fds,!1);let t=e.taID,n=[];return n.push({lbl:$bct.smd,fnc:()=>{$bam.smd(t)}}),n.push({lbl:$bct.ati,fnc:()=>{$bam.ati(t)}}),$("#topbar").ocmsmenu(n)},eM:(e,t,n)=>{let a=[];return!0===$fis.isAuth("fds_bam",2)&&a.push({lbl:$bct.upl,fnc:$bam.ubs}),!0===bool(e,!1)&&a.push({lbl:$bct.rel,fnc:$bam.renderLst}),$("#topbar").ocmsmenu(a)},prepLst:function(e){let t=new Date,n=$fis.lf(!0).ldng(1),a=new Date("2021-01-01");$fis.frm_list().IN((function(){}));$$.i({placeholder:$bct.in}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$bam.renderbt("i:"+n,"s"),t.val(""))})),$$.i({placeholder:$bct.bt}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$bam.renderbt("b:"+n,"s"),t.val(""))})),$$.i({placeholder:$bct.bv}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$bam.renderbt(`v:${n}`,"s"),t.val(""))})),$$.dc("mth oreq",n).text($bct.dqb).click((function(e){let t=$(this);e.stopPropagation(),t.siblings().rC("selected"),!0===t.is(".selected")&&(t.tC("selected"),$bam.renderLst(!1)),t.aC("selected")}));n.append("
");let l=$$.dc("mthl",n),i=t.getFullYear(),c=t.getMonth()+1;for(let t=a.getFullYear();t<=i;t++){let n=$$.dc("yr").prependTo(l).text(`${$bct.iov[e]} - ${t.toString()}`).toggleClass("selected",t===i);n.click({yr:t},(function(e){e.stopPropagation(),n.siblings().rC("selected"),n.aC("selected")}));let s=$$.dc("mfrm",n);for(let n=0;n<(t!==i?12:c);n++){a=new Date(t,n,1);let l=$$.dc("mth").prependTo(s).text(`${$bct.iov[e]} - ${fdt(a,"MMM yyyy")}`);l.click({yr:t,mt:n},(function(e){if(e.stopPropagation(),l.siblings().rC("selected"),!0===l.is(".selected")){l.tC("selected");let t=fdt(new Date(e.data.yr,e.data.mt,1),"yy-MM-dd");$bam.renderbt(t,"m")}l.aC("selected")}));let i=getMonday(a),c=new Date(a);c.setMonth(c.getMonth()+1),c.setDate(0),c=getMonday(c);let d=$$.dc("wfrm",l);for(;i<=c;){let e=$$.dc("wk",d).text(`${$bct.wk||"W"} ${fdt(i,"dd.MM.yy")}`);e.click({rd:new Date(i)},(function(t){t.stopPropagation();let n=fdt(t.data.rd,"yy-MM-dd");$bam.renderbt(n,"w"),l.siblings().rC("selected").find(".wk").rC("selected"),l.aC("selected").find(".wk").rC("selected"),e.aC("selected")})),i.setDate(i.getDate()+7)}}}n.ldng(0)},renderbt:function(e,t){let n=$fis.frm_list().ldng(1),a=$$.dc("invfrm",n).aC("md"+t),l=$fis.lf();$ocms.postXT({url:$ocms.url("bam/btl"),data:{mode:t,tgt:e},success:e=>{l.rC("fix").aC("hd"),$$.dc("ovhd",a).append($$.s(e.admin.title)).appendIf($$.sc("note",e.admin.note),""!==ne(e.admin.note,""));let t=$$.tblset({},a),n=$bcol.qtl,i=$$.tr(t.hd);$$.th(i);$.each(n.fields||[],((e,t)=>{$$.th(i).text(t.label),"vat"===t.name&&$$.th(i)}));let c=!1;$.each(e.bank||[],((e,a)=>{let i=$$.tr(t.bdy).tC("alt",c);i.click((function(){l.rC("fix").aC("hd"),i.tC("selected").siblings().rC("selected").find("td.av").rC("av"),i.find("td.av").rC("av")}));$$.td(i,{class:"raux"});$.each(n.fields||[],((e,t)=>{let n=$$.td(i).aC(t.dtype),l=a[t.name];if("function"==typeof t.dfnc)t.dfnc.call(n,l,a);else switch(t.type||""){case"date":n.text(fdt(a[t.name],"dd.MM.yy"));break;case"datetime":n.text(fdt(a[t.name]));break;case"html":n.append($$.dc("ctw").html(l)),n.append($$.dc("ttip").html(l));break;default:n.text(a[t.name])}switch(typeof t.title){case"function":t.title.call(n,a);break;case"string":n.attr("title",cs.title)}}))}))},complete:()=>{n.ldng(0)}})},renderLst:function(){let e=$fis.frm_list(!0,!0).ldng(1),t=$$.dc("bamfrm",e).aC("mdl"),n=$fis.lf();$bam.eM(!0),$ocms.postXT({url:$ocms.url("bam/qtl"),data:{mode:"l"},success:e=>{n.rC("fix").aC("hd"),$$.dc("ovhd",t).append($$.s(e.admin.title)).appendIf($$.sc("note",e.admin.note),""!==ne(e.admin.note,""));let a=$$.tblset({},t),l=$bcol.qtl,i=$$.tr(a.hd);$$.th(i);$.each(l.fields||[],((e,t)=>{$$.th(i).text(t.label)})),$.each(e.bs||[],((e,t)=>{let i=$$.tr(a.bdy,{id:`bt${t.taID}`});i.click((function(){n.rC("fix").aC("hd"),i.toggleClass("selected").siblings().rC("selected").find("td.av").rC("av"),i.find("td.av").rC("av"),!0===i.is(".selected")?$bam.iMn(t):$bam.eM(!0)}));$$.td(i,{class:"raux"});$.each(l.fields||[],((e,n)=>{let a=$$.td(i).aC(n.dtype);"select"===(n.type||"")?a.text((n.url||{})[t[n.name]]||""):a.text(t[n.name])}))}))},complete:()=>{e.ldng(0)}})},ubs:function(){$ocms.dlgform($bcol.bsu,{title:$bcol.bsu.label_sng||"",submit:function(e){var t=$(this).ldng(1),n=new FormData;let a=t.find(':input[name="bsu"]');a.length>0&&a[0].files.length>0?($.each(a[0].files,(function(e,t){n.append("bsu",t)})),$ocms.postXT({url:$ocms.url("bam/up"),data:n,success:function(e){t.trigger("modal_close")},error:function(){alert($t.l17)},complete:function(){t.ldng(0),$bam.renderLst()},timeout:0})):(options.success.call(this,n),t.trigger("modal_close"))},typedvalues:!0})},smd:e=>{confirm($bct.smdc)&&$ocms.postXT({url:$ocms.url("bam/smd"),data:{taid:e},success:t=>{$(`#bt${e}`).aC("mdone")}})},ati:e=>{let t=$$.dc("form"),n=$$.dc("form-body",t),a=$$.dc("form-group",n),l=($$.lbl($bct.ino,{for:"rno"}).appendTo(a),$$.i({id:"rno",placeholder:"Rechnungs-Nummer"}).appendTo(a)),i=$$.dc("rfrm bam",n);l.change((function(e){$ocms.postXT({url:$ocms.url("bam/vfi"),data:{invid:l.val()},success:e=>{i.empty(),e.length>0?($$.dc("nfo",i,$bct.fi),$.each(e,(function(e,t){let n=$$.dc("rinv",i,t.InvoiceId).data("inv",t).click((function(e){$(this).addClass("selected").siblings(".rinv").removeClass("selected")}));$$.dc("invsta",n).rwText(t.SendToAddress)}))):i.text($bct.ni)}})})),$ocms.dlg(t,{title:$bct.ati,size:[500,700],confirm:function(t){let n=$(this),a=n.find(".rinv.selected:first");if(1===a.length){let t=a.data("inv")||{};confirm(string($bct.atic,[t.InvoiceId]))&&$ocms.postXT({url:$ocms.url("bam/ati"),data:{taid:e,iid:t.Id},success:t=>{$(`#bt${e}`).aC("mdone"),n.trigger("modal_close")}})}}})}},$$bam={init2:$bam.init2,auth:{}};export default $$bam; \ No newline at end of file +let $bct={wk:"Woche",nd:"Keine Daten gefunden.",h:"Uhr",mdl:"BankBuchungen",upl:"BuchungsExport hochladen",rel:"Neu Laden",smd:"Buchung als erledigt markieren",smdc:"Buchung wirklich als erledigt markieren?\nDie Buchung wird nicht mehr in den auffälligen Buchungen angezeigt.",ati:"Buchung einer Rechnung zuordnen",ni:"Es wurden keine Rechnungen mit dieser ID gefunden.",ino:"Rechnungsnummer",fi:"Folgende Rechnungen wurden gefunden",atic:"Buchung wirklich der Rechnung {0} zuordnen?",dqb:"Auffällige Zahlungen anzeigen",iov:{all:"Buchungsübersicht (alle)","":"Buchungsübersicht"},in:"Rechnungsnummer",bt:"Buchungstexte",bv:"Buchungswert (volle €)",mbe:"Manuelle Buchung erfassen",mbc:"Manuelle Buchung ändern",mbd:"Manuelle Buchung löschen",mbdc:"Manuelle Buchung wirklich löschen?\nDieser Vorgang kann nicht rückgängig gemacht werden."},$bcol={man:new fields_definition("Manuelle Buchung","Manuelle Buchungen",[{name:"taID",type:"hidden"},{name:"ValueDate",label:"Valuta",type:"date",required:!0},{name:"Amount",label:"Betrag",type:"number",precision:"0.01",required:!0},{name:"NameOfPayer",label:"Name",type:"string"},{name:"SepaRemittanceInformation",label:"Verwendungszweck",type:"string"}]),qtl:new fields_definition("Kontobericht","Kontoberichte",[{name:"InvoiceId",label:"RechnungsNr",type:"string"},{name:"ValueDate",label:"Valuta",type:"date"},{name:"InvoiceBalance",label:"Rechnungsbetrag",type:"string"},{name:"IsCanceled",label:"Storno",type:"bool"},{name:"Amount",label:"Gutschrift",type:"string"},{name:"Skonto",label:"Skonto ?",type:"bool"},{name:"Deviation",label:"Abweichung [%]",type:"bool"},{name:"AccountNumberOfPayer",label:"IBAN",type:"string"},{name:"NameOfPayer",label:"Name",type:"string"},{name:"SepaRemittanceInformation",label:"Verwendungszweck",type:"string"},{name:"EndToEndReference",label:"Referenz",type:"string"}]),bsu:new fields_definition("Kontobericht","Kontoberichte",[{name:"bsu",label:"Export der Buchungen",type:"file",required:!0,prop:{multiple:!0}}])},gi=(e,t)=>$$.sc(`glyphicon glyphicon-${e}`).aC(t),$bam={init2:function(e,t){e=e||"bam",t=t||{},$ocms.getScript([],(function(){$bam.init3(e,t)}))},init3:async function(e,t){$fis.cf(!0);let n=$fis.lf(!0);$bam.eM(),$("#activemodule").text($bct.mdl),await $fis.prepAuth("fds_bam,fds_inv,fds_reminder");let a=[(async()=>{!0===$fis.isAuth("fds_bam",1)&&($bam.prepLst(""),n.aC("fix"))})(),(async()=>{!0===$fis.isAuth("fds_bam",1)&&$bam.renderLst(!1)})(),new Promise(((e,t)=>{}))];await Promise.all(a)},iMn:e=>{bool(e.fds,!1);let t=e.taID,n=[];return n.push({lbl:$bct.smd,fnc:()=>{$bam.smd(t)}}),n.push({lbl:$bct.ati,fnc:()=>{$bam.ati(t)}}),!0===bool(e.IsManual,!1)&&(n.push({lbl:$bct.mbc,fnc:()=>{$bam.mbc(t)}}),n.push({lbl:$bct.mbd,fnc:()=>{$bam.mbd(t)}})),$("#topbar").ocmsmenu(n)},eM:(e,t,n)=>{let a=[];return!0===$fis.isAuth("fds_bam",2)&&(a.push({lbl:$bct.upl,fnc:$bam.ubs}),a.push({lbl:$bct.mbe,fnc:$bam.mbe})),!0===bool(e,!1)&&a.push({lbl:$bct.rel,fnc:$bam.renderLst}),$("#topbar").ocmsmenu(a)},prepLst:function(e){let t=new Date,n=$fis.lf(!0).ldng(1),a=new Date("2021-01-01");$fis.frm_list().IN((function(){}));$$.i({placeholder:$bct.in}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$bam.renderbt("i:"+n,"s"),t.val(""))})),$$.i({placeholder:$bct.bt}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$bam.renderbt("b:"+n,"s"),t.val(""))})),$$.i({placeholder:$bct.bv}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$bam.renderbt(`v:${n}`,"s"),t.val(""))})),$$.dc("mth oreq",n).text($bct.dqb).click((function(e){let t=$(this);e.stopPropagation(),t.siblings().rC("selected"),!0===t.is(".selected")&&(t.tC("selected"),$bam.renderLst(!1)),t.aC("selected")}));n.append("
");let l=$$.dc("mthl",n),c=t.getFullYear(),i=t.getMonth()+1;for(let t=a.getFullYear();t<=c;t++){let n=$$.dc("yr").prependTo(l).text(`${$bct.iov[e]} - ${t.toString()}`).toggleClass("selected",t===c);n.click({yr:t},(function(e){e.stopPropagation(),n.siblings().rC("selected"),n.aC("selected")}));let s=$$.dc("mfrm",n);for(let n=0;n<(t!==c?12:i);n++){a=new Date(t,n,1);let l=$$.dc("mth").prependTo(s).text(`${$bct.iov[e]} - ${fdt(a,"MMM yyyy")}`);l.click({yr:t,mt:n},(function(e){if(e.stopPropagation(),l.siblings().rC("selected"),!0===l.is(".selected")){l.tC("selected");let t=fdt(new Date(e.data.yr,e.data.mt,1),"yy-MM-dd");$bam.renderbt(t,"m")}l.aC("selected")}));let c=getMonday(a),i=new Date(a);i.setMonth(i.getMonth()+1),i.setDate(0),i=getMonday(i);let d=$$.dc("wfrm",l);for(;c<=i;){let e=$$.dc("wk",d).text(`${$bct.wk||"W"} ${fdt(c,"dd.MM.yy")}`);e.click({rd:new Date(c)},(function(t){t.stopPropagation();let n=fdt(t.data.rd,"yy-MM-dd");$bam.renderbt(n,"w"),l.siblings().rC("selected").find(".wk").rC("selected"),l.aC("selected").find(".wk").rC("selected"),e.aC("selected")})),c.setDate(c.getDate()+7)}}}n.ldng(0)},renderbt:function(e,t){let n=$fis.frm_list().ldng(1),a=$$.dc("invfrm",n).aC("md"+t),l=$fis.lf();$ocms.postXT({url:$ocms.url("bam/btl"),data:{mode:t,tgt:e},success:e=>{l.rC("fix").aC("hd"),$$.dc("ovhd",a).append($$.s(e.admin.title)).appendIf($$.sc("note",e.admin.note),""!==ne(e.admin.note,""));let t=$$.tblset({},a),n=$bcol.qtl,c=$$.tr(t.hd);$$.th(c);$.each(n.fields||[],((e,t)=>{$$.th(c).text(t.label),"vat"===t.name&&$$.th(c)}));let i=!1;$.each(e.bank||[],((e,a)=>{let c=$$.tr(t.bdy).tC("alt",i);c.click((function(){l.rC("fix").aC("hd"),c.tC("selected").siblings().rC("selected").find("td.av").rC("av"),c.find("td.av").rC("av")}));$$.td(c,{class:"raux"});$.each(n.fields||[],((e,t)=>{let n=$$.td(c).aC(t.dtype),l=a[t.name];if("function"==typeof t.dfnc)t.dfnc.call(n,l,a);else switch(t.type||""){case"date":n.text(fdt(a[t.name],"dd.MM.yy"));break;case"datetime":n.text(fdt(a[t.name]));break;case"html":n.append($$.dc("ctw").html(l)),n.append($$.dc("ttip").html(l));break;default:n.text(a[t.name])}switch(typeof t.title){case"function":t.title.call(n,a);break;case"string":n.attr("title",cs.title)}}))}))},complete:()=>{n.ldng(0)}})},renderLst:function(){let e=$fis.frm_list(!0,!0).ldng(1),t=$$.dc("bamfrm",e).aC("mdl"),n=$fis.lf();$bam.eM(!0),$ocms.postXT({url:$ocms.url("bam/qtl"),data:{mode:"l"},success:e=>{n.rC("fix").aC("hd"),$$.dc("ovhd",t).append($$.s(e.admin.title)).appendIf($$.sc("note",e.admin.note),""!==ne(e.admin.note,""));let a=$$.tblset({},t),l=$bcol.qtl,c=$$.tr(a.hd);$$.th(c);$.each(l.fields||[],((e,t)=>{$$.th(c).text(t.label)})),$.each(e.bs||[],((e,t)=>{let c=$$.tr(a.bdy,{id:`bt${t.taID}`});c.click((function(){n.rC("fix").aC("hd"),c.toggleClass("selected").siblings().rC("selected").find("td.av").rC("av"),c.find("td.av").rC("av"),!0===c.is(".selected")?$bam.iMn(t):$bam.eM(!0)}));$$.td(c,{class:"raux"});$.each(l.fields||[],((e,n)=>{let a=$$.td(c).aC(n.dtype);"select"===(n.type||"")?a.text((n.url||{})[t[n.name]]||""):a.text(t[n.name])}))}))},complete:()=>{e.ldng(0)}})},ubs:function(){$ocms.dlgform($bcol.bsu,{title:$bcol.bsu.label_sng||"",submit:function(e){var t=$(this).ldng(1),n=new FormData;let a=t.find(':input[name="bsu"]');a.length>0&&a[0].files.length>0?($.each(a[0].files,(function(e,t){n.append("bsu",t)})),$ocms.postXT({url:$ocms.url("bam/up"),data:n,success:function(e){t.trigger("modal_close")},error:function(){alert($t.l17)},complete:function(){t.ldng(0),$bam.renderLst()},timeout:0})):(options.success.call(this,n),t.trigger("modal_close"))},typedvalues:!0})},smd:e=>{confirm($bct.smdc)&&$ocms.postXT({url:$ocms.url("bam/smd"),data:{taid:e},success:t=>{$(`#bt${e}`).aC("mdone")}})},ati:e=>{let t=$$.dc("form"),n=$$.dc("form-body",t),a=$$.dc("form-group",n),l=($$.lbl($bct.ino,{for:"rno"}).appendTo(a),$$.i({id:"rno",placeholder:"Rechnungs-Nummer"}).appendTo(a)),c=$$.dc("rfrm bam",n);l.change((function(e){$ocms.postXT({url:$ocms.url("bam/vfi"),data:{invid:l.val()},success:e=>{c.empty(),e.length>0?($$.dc("nfo",c,$bct.fi),$.each(e,(function(e,t){let n=$$.dc("rinv",c,t.InvoiceId).data("inv",t).click((function(e){$(this).addClass("selected").siblings(".rinv").removeClass("selected")}));$$.dc("invsta",n).rwText(t.SendToAddress)}))):c.text($bct.ni)}})})),$ocms.dlg(t,{title:$bct.ati,size:[500,700],confirm:function(t){let n=$(this),a=n.find(".rinv.selected:first");if(1===a.length){let t=a.data("inv")||{};confirm(string($bct.atic,[t.InvoiceId]))&&$ocms.postXT({url:$ocms.url("bam/ati"),data:{taid:e,iid:t.Id},success:t=>{$(`#bt${e}`).aC("mdone"),n.trigger("modal_close")}})}}})},mbe:()=>{$bam.manForm({})},mbc:e=>{$ocms.postXT({url:$ocms.url("bam/mget"),data:{taid:e},success:e=>{$bam.manForm(e||{})}})},mbd:e=>{confirm($bct.mbdc)&&$ocms.postXT({url:$ocms.url("bam/mdel"),data:{taid:e},success:()=>{$bam.renderLst()}})},manForm:e=>{let t=$bcol.man.clone().applyValues(e||{});$ocms.dlgform(t,{title:""!==ne(e.taID,"")?$bct.mbc:$bct.mbe,url:$ocms.url("bam/man"),typedvalues:!0,success:()=>{$bam.renderLst()}})}},$$bam={init2:$bam.init2,auth:{}};export default $$bam; \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.inv.css b/Fuchs/wwwroot/web/fis.inv.css index bbd98a4..b31e3cd 100644 --- a/Fuchs/wwwroot/web/fis.inv.css +++ b/Fuchs/wwwroot/web/fis.inv.css @@ -156,6 +156,16 @@ table.if td.num { height: auto; cursor: pointer; } +.edit_frm .invoice_layout .emailfrm .content { + color: #000; +} +.edit_frm .invoice_layout .emailfrm .content:empty::before { + content: "-"; + min-width: 5rem; + height: 1rem; + display: block; + position: absolute; +} .edit_frm .invoice_layout .emailfrm:hover div.axf { display: block; } @@ -285,6 +295,13 @@ table.if td.num { border-color: #FFF; border-top: 1.5px solid #000; } +.edit_frm .invoice_layout table.invi > tbody > tr { + /* Set-pricing display (see InvoiceSetPricing / $inv.d.applySetDisplay): the set + header line is emphasised the same way the PDF renders it (bold title). */ +} +.edit_frm .invoice_layout table.invi > tbody > tr.sethdr td.txt { + font-weight: bold; +} .edit_frm .invoice_layout table.invi th.currency, .edit_frm .invoice_layout table.invi th.num, .edit_frm .invoice_layout table.invi td.currency, .edit_frm .invoice_layout table.invi td.num { text-align: right; white-space: nowrap; @@ -405,6 +422,50 @@ table.if td.num { animation: fis_spin 0.8s linear infinite; } +/* Backend-authoritative draft editing (ADR 0006): server-side plausibility findings + rendered above the editor, and the change-history dialog table. */ +.edit_frm .invoice_layout .dvalidation { + margin: 0 0 1rem 0; +} +.edit_frm .invoice_layout .dvalidation.hidden { + display: none; +} +.edit_frm .invoice_layout .dvalidation .dvmsg { + padding: 0.4rem 0.7rem; + margin: 0.25rem 0; + border-radius: 0.2rem; + font-size: 0.9rem; + border-left: 4px solid #999; + background: #f5f5f5; +} +.edit_frm .invoice_layout .dvalidation .dvmsg.error { + border-left-color: #c0392b; + background: #fdecea; + color: #922; +} +.edit_frm .invoice_layout .dvalidation .dvmsg.warning { + border-left-color: #e0a800; + background: #fff8e1; + color: #7a5c00; +} +.edit_frm .invoice_layout .dvalidation .dvmsg.info { + border-left-color: #3498db; + background: #eaf4fb; + color: #1c5a82; +} + +.dhist .invtbl { + width: 100%; +} +.dhist td, .dhist th { + padding: 0.3rem 0.6rem; + text-align: left; + vertical-align: top; +} +.dhist tbody tr:nth-child(even) { + background: #fafafa; +} + .modal-body .lstfrm { display: block; position: relative; diff --git a/Fuchs/wwwroot/web/fis.inv.de.js b/Fuchs/wwwroot/web/fis.inv.de.js index 5067862..4f5ec23 100644 --- a/Fuchs/wwwroot/web/fis.inv.de.js +++ b/Fuchs/wwwroot/web/fis.inv.de.js @@ -34,6 +34,9 @@ let $rct = { svcPart: 'Service-Anteil', vat: 'Umsatzsteuer', combP: 'Positionen zusammenfassen', + toSetP: 'Auf Setpreis umstellen (Positionen ohne Preis)', + toSetPc: 'Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?', + toSetMc: 'Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?', iSum: 'Zwischensumme', dtRel: 'Freigegeben am: ', dtCr: 'Erstellt am: ', @@ -149,7 +152,6 @@ let $ict = { setm: 'Set-Preisanzeige', setmo: { setprice: 'Set mit Preis – Positionen ohne Preis', - itemprices: 'Positionen mit Preis – Set als Überschrift', setonly: 'Nur Set mit Preis – Positionen ausgeblendet' }, ctp: 'Ansprechpartner festlegen', @@ -169,16 +171,10 @@ let $ict = { rovlh: 'Übersicht der bisherigen Mahnungen', rovl: ['Betreff', 'Betrag', 'Betrag gezahlt', 'fertiggestellt am'], remHR: ['Rechnung', 'vom', 'Rechnungsbetrag', 'bereits bezahlt', 'noch offen'], - remt: { - f: ['Sehr geehrte Damen und Herren,', 'ein Mahnschreiben sollte kurz, freundlich und erfolgreich sein. Kurz ist es, freundlich sowieso; ob es auch erfolgreich ist, hängt von Ihnen ab.'] - , m: ['Sehr geehrte Damen und Herren,', 'nun müssen wir Sie noch einmal anschreiben.', 'Wahrscheinlich haben Sie triftige Gründe dafür, warum Sie die Zahlung unserer Forderung nicht vornehmen und auch nicht auf unsere Mahnung reagieren. Sollten wir darüber nicht einmal sprechen?', 'Bitte nehmen Sie umgehend in dieser Sache mit uns Kontakt auf.'] - , l: ['Sehr geehrte Damen und Herren,', 'Eine DRITTE MAHNUNG zu erhalten bereitet Ihnen bestimmt ebenso wenig Freude wie uns, sie zu verschicken. Leider haben wir auf unsere zweite Mahnung noch keine Antwort von Ihnen erhalten.", "Wir bitten Sie, den offenen Betrag innerhalb der nächsten 7 Werktage nach Erhalt dieses Schreibens zu begleichen. Nach Ablauf dieser Frist erfolgt keine weitere Mahnung mehr.', 'Sollte die Forderung bis dahin nicht beglichen sein, eröffnen wir das gerichtliche Mahnverfahren. Sollten Sie die Rechnung inzwischen beglichen haben, so betrachten Sie bitte dieses Schreiben als gegenstandslos.'] - }, - remt2: { - f: ['Wir bitten Sie, den noch offenen Rechnungsbetrag innerhalb einer Woche auf unser Konto zu überweisen.', 'Sollten Sie den Betrag bereits überwiesen haben, so bitten wir Sie, diese Zahlungserinnerung als gegenstandslos zu betrachten.'] - , m: ['Um Ihnen zusätzliche Kosten für weitere Mahnungen zu ersparen, bitten wir Sie nunmehr um die Überweisung des noch zu zahlenden Gesamtbetrages inklusive der ggf. bereits fälligen Mahnzinsen und Mahngebühren innerhalb von einer Woche.'] - , l: [] - }, payi: { account: 'Konto', name: 'Zahler', text: 'Verw.Zweck', InvoiceID: 'Rechnung', amount: 'Betrag', date: 'Datum', manual: 'Typ'} + // Reminder intro/closing notice texts are backend-authoritative (see FuchsPdf.BuildReminderNotes / + // $inv.rd.notes) — no client-side copies here. + translatePaymentTerm: (pt) => (pt || '').replace('wd', ' Werktagen').replace('d', ' Tagen').replace('wk', ' Wochen') || '10 Tagen' + , payi: { account: 'Konto', name: 'Zahler', text: 'Verw.Zweck', InvoiceID: 'Rechnung', amount: 'Betrag', date: 'Datum', manual: 'Typ'} }, $invcol = { datev: new fields_definition('Rechnung', 'Rechnungen', [ { name: 'Umsatz (ohne Soll/Haben-Kz)', label: 'Umsatz (ohne Soll/Haben-Kz)', type: 'string' }, @@ -629,6 +625,11 @@ $inv.iMn = (ix) => { } return $('#topbar').ocmsmenu(m); } +/* Menu option CSV builder: 'setm' (Set-Preisanzeige) is only offered while the invoice has not + yet been explicitly switched to a set-display mode (admin.setmode empty/undefined) — once set, + changing it again is not supported (the user edits the line items directly instead); it + reappears only if 'Änderungen verwerfen' reverts to a DB state that was never switched. */ +$inv.emOpt = (admin) => 'iss,p13b,ctp' + (((admin || {}).setmode || '') === '' ? ',setm' : ''); $inv.eM = (r, re, opt) => { let m = []; if (booln(r, false) === true || booln(re, false) === true) { @@ -646,11 +647,507 @@ $inv.eM = (r, re, opt) => { if ((opt || '').split(',').includes('setm') === true) { m.push({ lbl: $ict.setm, fnc: $inv.ssetmode }); } + if ((opt || '').split(',').includes('iss') === true) { /* backend-authoritative draft (ADR 0006) */ + m.push({ lbl: 'Änderungshistorie', fnc: () => $inv.d.history() }); + m.push({ lbl: 'Änderungen verwerfen', fnc: () => $inv.d.discard() }); + } if (booln(r, false) === true) { m.push({ lbl: $ict.rel, fnc: $inv.rReload }); } return $('#topbar').ocmsmenu(m); /* empty Array => empty menu */ }; +/* ── Backend-authoritative draft editing controller (ADR 0006/0007) ─────────── + The server holds the truth for an invoice draft in an in-memory session; this + object seeds it, sends single edits as deltas, and renders the totals footer + + validation from the authoritative server state. It is invoice-only: reminders + never obtain a token (invSumUpdate, the seed trigger, is only bound for invoices), + so they keep the legacy stateless flow untouched. */ +$inv.d = { + tbl: () => $('div.invoice_layout table.invi'), + layout: () => $('div.invoice_layout'), + token: function () { return $inv.d.tbl().data('dtoken') || ''; }, + /* Hash each assembled block so only genuinely-changed blocks are sent as deltas. */ + hashes: function () { + let bai = $inv.d.tbl().data('bai') || [], h = {}; + $.each(bai, (i, b) => { h[(b.Id || '').toString()] = JSON.stringify(b); }); + return h; + }, + /* Seed the authoritative server session from the assembled editor payload. Called + once from invSumUpdate on open; thereafter the backend is the source of truth. + The in-flight ajax promise is stashed on 'dseedpromise' so a delta sent (e.g. via + a context-menu action) before seeding has actually completed can wait for the token + instead of silently no-op'ing (see $inv.d.sync). */ + seed: function (payload) { + let l = $inv.d.layout(); l.aC('freeze'); + let p = $ocms.postXT({ + url: $ocms.url('inv/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => { + $inv.d.tbl().data('dtoken', r.token).data('dver', r.version).data('dhashes', $inv.d.hashes()).data('dorder', $inv.d.order()); + $fis.draft.bind(r.token, { + onReady: () => $inv.d.refresh(), + onExpiring: (s) => $inv.d.warnExpiry(s), + onClosed: (reason) => $inv.d.closed(reason) + }); + $inv.d.refresh(); + }, error: () => { l.rC('freeze'); }, complete: () => { $inv.d.tbl().removeData('dseeding').removeData('dseedpromise'); } + }); + $inv.d.tbl().data('dseedpromise', p); + return p; + }, + /* Re-fetch the authoritative state and render the totals footer + validation from it. */ + refresh: function (cb) { + let t = $inv.d.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('inv/dstate'), data: { token: t }, success: (state) => { + $inv.d.applyState(state); if (typeof cb === 'function') { cb(state); } + }, error: (xhr) => { if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } }, + complete: () => { $inv.d.layout().rC('freeze'); } + }); + }, + applyState: function (state) { + let tbl = $inv.d.tbl(); if (tbl.length < 1) { return; } + /* Concurrent refreshes (one per flushed block plus one for a follow-up delta such as + item.setprice, and SignalR draftReady on top) can have their dstate responses arrive + out of order. Applying a response older than the version already rendered would + silently roll back a just-applied server-side mutation (e.g. a set-price conversion + reverting to its pre-conversion 0/individually-priced state) — so stale responses are + dropped here instead of rendered. */ + let curVer = tbl.data('dver'); + if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) { + return; + } + tbl.data('dver', state.version).data('serverSums', state.sums); + $inv.d.footer(tbl, state.sums || {}, state.admin || {}); + $inv.d.notes(state.notes || []); + $inv.d.validation(state.validation || []); + $inv.d.applyItems(tbl, state.req || []); + $inv.d.applySetDisplay(tbl, state.setDisplay || {}); + /* Keep the menu's "Set-Preisanzeige" entry in sync with the authoritative admin.setmode: + hides it right after a mode switch, and restores it if a discard reverted the draft to + a DB state that was never switched (ADR 0006 backend-authoritative refresh). */ + $inv.eM(false, true, $inv.emOpt(state.admin || {})); + }, + /* Renders the backend-authoritative notice paragraphs (ADR: notice texts are + backend-authoritative) exactly as FuchsPdf.BuildInvoiceNotes emits them for the PDF, + so the online editor always shows precisely what will be printed. */ + notes: function (notes) { + let frm = $inv.d.layout(); if (frm.length < 1) { return; } + let box = frm.children('.dnotes'); + if (box.length < 1) { box = $$.dc('dnotes'); frm.append(box); } + box.empty(); + (notes || []).forEach((n) => $$.dc('dnote').aC(n.style || 'InvoiceNotes').text(n.text)); + }, + /* Push the server's authoritative position numbers back onto the rendered rows so the online + editor and the PDF preview always agree (the server numbers priced lines continuously; the + browser must not keep its own numbering). Only the position cell is touched — no re-render. */ + /* Reconciles each block's rendered rows against the authoritative server line list + (`req[].itm`, the co shape). The server may not only change a line's values but also + ADD lines (the block set row inserted by "Set mit Preis"/block.setprice) and REMOVE lines + ("Nur Set mit Preis"/block.setonly), and reorder them — the browser never computes any of + that (ADR 0006/0008/0009), it just mirrors the server. For each server line: patch/insert + the matching row, preserving null price fields verbatim (ADR 0009: null = empty cell, kept + distinct from 0). Rows the server no longer lists are removed; order follows the server. + Position numbers, net/VAT and the set-row emphasis all come from rrw off the mirrored data. */ + applyItems: function (tbl, req) { + let val = (x) => (x === undefined ? null : x); // JSON null stays null; absent -> null + (req || []).forEach((b) => { + if (!b) { return; } + let bid = (b.Id || '').toString(); if (bid === '') { return; } + let bdy = tbl.children('tbody').filter((i, e) => (($(e).data() || {}).Id || '').toString() === bid).first(); + if (bdy.length < 1) { return; } + let itms = b.itm || [], serverIds = {}; + let title = bdy.find('tr.title').first(); + let prev = title.length ? title : null; + itms.forEach((co) => { + let id = (co.id || '').toString(); if (id === '') { return; } + serverIds[id] = true; + let rw = bdy.find('#itm' + id).first(), isNew = rw.length < 1; + if (isNew) { rw = $$.tr(bdy, { id: 'itm' + id, class: 'itm' }); } + let dta = rw.data() || {}; + let changed = isNew || (dta.Type || '') !== (co.typ || '') + || dta.net !== val(co.v) || dta.net_val !== val(co.vt) || dta.vat_val !== val(co.vv) + || dta.svcnet_val !== val(co.vs) || dta.svcvat_val !== val(co.vsv) + || (dta.position || '') !== (co.p != null ? co.p : ''); + $.extend(dta, { + Id: id, Type: co.typ, + net: val(co.v), net_val: val(co.vt), vat_val: val(co.vv), + svcnet_val: val(co.vs), svcvat_val: val(co.vsv), + vat: co.vat, position: (co.p != null ? co.p : ''), + SetItmId: (co.SetItmId != null ? co.SetItmId : null) + }); + if (isNew) { $.extend(dta, { htmltext: co.t, quantity: co.q }); } // text only for server-created rows (e.g. the set row) + rw.data(dta); + if (prev) { if (rw.prev()[0] !== prev[0]) { rw.insertAfter(prev); } } // keep server order, but don't churn already-ordered rows + else if (bdy.children().first()[0] !== rw[0]) { bdy.prepend(rw); } + prev = rw; + if (changed) { $inv.rrw.call(rw); } + }); + bdy.find('tr.itm').each((i, tr) => { + let id = (($(tr).data() || {}).Id || '').toString(); + if (id !== '' && !serverIds[id]) { $(tr).remove(); } // dropped server-side (block.setonly) + }); + }); + }, + /* Applies the backend-authoritative set-pricing display flags (see BuildSetDisplay / + InvoiceSetPricing) onto the rendered rows: blanks the price/total cells for lines the + server says should show no price (set members in SetPrice/SetOnly mode), and emphasises + the set header line — so the online editor always shows exactly what the PDF will print, + without duplicating the pricing rules client-side. Rows not present in the map (no set in + that block, or standalone items) are left untouched. */ + applySetDisplay: function (tbl, setDisplay) { + tbl.find('tr.itm').each((i, tr) => { + let rw = $(tr), dta = rw.data() || {}, id = (dta.Id || '').toString(); + if (id === '' || !Object.prototype.hasOwnProperty.call(setDisplay, id)) { return; } + let flags = setDisplay[id] || {}; + rw.tC('sethdr', bool(flags.h, false)); + if (bool(flags.p, true) === false) { + rw.find('td.currency').text(''); + } + }); + }, + /* Send one change to the server; the draftReady signal and this success both refresh. + Returns the underlying ajax promise so callers that must not race a follow-up delta + against this one (e.g. $inv.toSetPrice after a flush) can chain on it. + If the initial seeding (inv/dopen) is still in flight (no token yet, but a + 'dseedpromise' is pending), the delta previously vanished silently — the user saw no + network activity at all when clicking a context-menu action right after opening the + editor. Now the delta waits for the seed to finish and retries against the freshly + issued token instead of being dropped. */ + sync: function (delta) { + let t = $inv.d.token(); + if (t === '') { + let seeding = $inv.d.tbl().data('dseedpromise'); + if (seeding) { return $.when(seeding).then(() => $inv.d.sync(delta)); } + return $.when(); + } + $inv.d.layout().aC('freeze'); + return $ocms.postXT({ + url: $ocms.url('inv/dpatch'), data: { token: t, delta: JSON.stringify(delta) }, + success: () => { $inv.d.refresh(); }, + error: (xhr) => { $inv.d.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.d.closed('expired'); } } + }); + }, + /* The current section id sequence (used to detect a reorder that changes no block content). */ + order: function () { return (($inv.d.tbl().data('bai')) || []).map((b) => (b.Id || '').toString()); }, + /* Diff the freshly-rebuilt blocks against the last-synced state and send only the + changed/removed blocks as granular block.replace / block.remove deltas. A pure section + reorder (same blocks, new sequence) changes no block hash, so it is sent separately as a + block.order delta; the server reorders the cache, renumbers positions and pushes them back. + No early return on an empty token here: sync() itself waits for an in-flight seed + (dseedpromise) instead of silently dropping the delta, so edits made in the brief window + before seeding completes are not lost. */ + syncChanged: function (tbl) { + let bai = tbl.data('bai') || [], prev = tbl.data('dhashes') || {}, next = {}, changed = [], removed = [], pending = []; + $.each(bai, (i, b) => { let id = (b.Id || '').toString(), h = JSON.stringify(b); next[id] = h; if (prev[id] !== h) { changed.push(b); } }); + $.each(prev, (id) => { if (next[id] === undefined) { removed.push(id); } }); + let order = $inv.d.order(), prevOrder = tbl.data('dorder') || []; + tbl.data('dhashes', next).data('dorder', order); + changed.forEach((b) => pending.push($inv.d.sync({ Target: 'block.replace', Ref: (b.Id || '').toString(), Value: b }))); + removed.forEach((id) => pending.push($inv.d.sync({ Target: 'block.remove', Ref: id }))); + let sameSet = prevOrder.length === order.length && prevOrder.slice().sort().join(',') === order.slice().sort().join(','); + if (sameSet && prevOrder.join(',') !== order.join(',')) { pending.push($inv.d.sync({ Target: 'block.order', Value: order })); } + /* $.when(...pending) so a caller (e.g. toSetPrice) can wait until every flushed block + has actually reached the server before sending a further delta that depends on it — + otherwise a delayed block.replace carrying pre-conversion values can land after and + silently overwrite a just-applied server-side mutation (e.g. item.setprice). */ + return $.when.apply($, pending).promise(); + }, + /* Map an inline recipient field to its delta target and send it. No token-guard here for the + same reason as syncChanged: sync() waits for an in-flight seed instead of dropping the edit. */ + syncField: function (nme, val) { + let map = { invoicetitle: 'title', invoiceaddress: 'address', invoiceemail: 'email', loc: 'provisionlocation', provisionlocation: 'provisionlocation', provisionperiod: 'provisionperiod' }; + let target = map[nme]; if (!target) { return; } + $inv.d.sync({ Target: target, Value: val }); + }, + /* Render the totals footer from the server sums (port of invSumUpdate's footer half). */ + footer: function (tbl, sums, admin) { + let ft = tbl.children('tfoot').empty(); tbl.nextAll('.fnote').remove(); + let p13b = bool(admin.p13b, false); + let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst)); + let fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t); + rwcy('Netto', sums.total_net || 0); + if (p13b === false) { $.each(sums.vat || {}, (rate, amt) => rwcy($rct.vat + ' ' + rate + '%', amt, 'tvat')); } + rwcy('Summe', sums.total_gross || 0); + let itype = (admin.type || ''); + if (itype === 'i') { fn($rct.note2); fn($rct.note4); } + else if (itype === 'c') { fn($rct.note2); } + else { + fn(string($rct.note3, [fnum(((sums.service_net || 0) + (sums.service_vat || 0)) * (admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax'); + fn($rct.note2); + fn(string($rct.note1, [fnum((sums.service_net || 0) + (sums.service_vat || 0), $rct.cst), fnum(sums.service_net || 0, $rct.cst), fnum(sums.service_vat || 0, $rct.cst)])); + } + if (p13b === true) { fn($rct.note13b); } + }, + validation: function (msgs) { + let frm = $('div.invoice_layout'); if (frm.length < 1) { return; } + let box = frm.children('.dvalidation'); + if (box.length < 1) { box = $$.dc('dvalidation'); frm.prepend(box); } + box.empty().tC('hidden', (msgs || []).length < 1); + $.each(msgs || [], (i, m) => $$.dc('dvmsg', box).aC(m.severity).text(m.message)); + }, + /* PDF preview straight from the cache; confirm = flush + finalise, cancel = discard. */ + preview: function () { + let t = $inv.d.token(); if (t === '') { return; } + let l = $inv.d.layout(); + let email = ($inv.d.tbl().data('new') || {}).invoiceemail || ''; + if ($fis.ValidateEmail(email) === false) { if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { return; } } + l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('inv/dpreview'), data: { token: t }, success: (response) => { + l.rC('freeze'); + let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), total = response.total; + if (total > 10) { $$.dc('note warn', c).text($ict.tpe); } + $.each(response.img || [], (ii, img) => { $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); }); + for (let ic = (response.img || []).length + 1; ic <= total; ic++) { $$.dc('pdfp ph', c).append($$.dc('note', $ict.pna)); } + $ocms.dlg(c, { + size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $rct.crI, + confirm: function (e) { + let ct = $(this); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('inv/dsave'), data: { token: t }, success: (sv) => { + $ocms.postXT({ + url: $ocms.url('req/sconf'), data: { id: sv.invid }, success: (cresp) => { + ct.trigger('modal_close'); + if (cresp.hasFile === true) { window.open($ocms.url('req/idoc') + '?id=' + sv.invid, '_blank'); } + $inv.d.close(); + $ocms.init('req'); $inv.rReload(); + }, error: () => { alert($t.f1); ct.trigger('modal_close'); }, complete: () => { l.rC('freeze'); } + }); + }, error: () => { l.rC('freeze'); alert($ict.eis); } + }); + }, + cancel: function (e) { if (confirm($ict.cdI)) { $inv.d.close(); $inv.rReload(); } } + }); + }, error: () => { l.rC('freeze'); alert($ict.eis); } + }); + }, + /* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */ + save: function () { + let t = $inv.d.token(); if (t === '') { return; } + let l = $inv.d.layout(); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('inv/dsave'), data: { token: t }, success: (r) => { $inv.d.tbl().data('invid', r.invid); }, + error: () => { alert($ict.eis); }, complete: () => { l.rC('freeze'); } + }); + }, + history: function () { + let t = $inv.d.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('inv/dhistory'), data: { token: t }, success: (r) => { + let c = $$.dc('dhist'); + if ((r.history || []).length < 1) { $$.dc('note', c).text('Noch keine Änderungen erfasst.'); } + else { + let ts = $$.tblset({ class: 'invtbl fullwidth' }, c); + $$.tr(ts.hd).append([$$.th().text('Zeit'), $$.th().text('Feld'), $$.th().text('Alt'), $$.th().text('Neu')]); + $.each(r.history, (i, h) => $$.tr(ts.bdy).append([$$.tdc('keep', fdt(h.timestamp)), $$.td().text(h.target), $$.td().text(h.oldValue), $$.td().text(h.newValue)])); + } + $ocms.dlg(c, { width: 800, form: false }); + } + }); + }, + discard: function () { + let invid = $inv.d.tbl().data('invid') || ''; + if (invid === '') { alert('Es wurde noch kein Zwischenstand gespeichert, der wiederhergestellt werden könnte.'); return; } + if (confirm('Alle Änderungen verwerfen und den zuletzt gespeicherten Stand neu laden?') === false) { return; } + $inv.d.close(); + $inv.cntInv({ id: invid }); + }, + warnExpiry: function (secondsLeft) { + let mins = Math.max(1, Math.round((secondsLeft || 0) / 60)); + $fis.notifications.push({ severity: 'info', title: 'Entwurf läuft ab', message: 'Der Rechnungsentwurf läuft in etwa ' + mins + ' Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren.' }); + }, + closed: function (reason) { + let t = $inv.d.token(); + $inv.d.tbl().removeData('dtoken'); + if (t !== '') { $fis.draft.release(t); } + $fis.frm_edit().remove(); $fis.lf(true); + $fis.notifications.push({ severity: 'error', title: 'Entwurf geschlossen', message: reason === 'expired' ? 'Der Rechnungsentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.' : 'Der Rechnungsentwurf wurde geschlossen.' }); + try { $inv.rReload(); } catch (e) { } + }, + close: function () { + let t = $inv.d.token(); + if (t !== '') { $ocms.postXT({ url: $ocms.url('inv/dclose'), data: { token: t } }); $fis.draft.release(t); } + $inv.d.tbl().removeData('dtoken'); + } +}; +/* ── Backend-authoritative reminder draft editing (ADR 0006/0007) ───────────── + The reminder mirror of $inv.d: the server holds the truth for a reminder draft in + an in-memory session; this object seeds it (rem/dopen), sends single edits as deltas + (rem/dpatch), and renders the open-amount footer + validation from the authoritative + server state (rem/dstate). Preview renders straight from the cache (rem/dpreview); + confirm flushes (rem/dsave) then finalises + emails (rem/conf). It coexists with $inv.d + on the same DOM: each keys off its own token (rdtoken vs dtoken), so the shared inline + editor safely no-ops for the mode that is not active. */ +$inv.rd = { + tbl: () => $('div.invoice_layout table.invi'), + layout: () => $('div.invoice_layout'), + token: function () { return $inv.rd.tbl().data('rdtoken') || ''; }, + /* Seed the authoritative server session from the assembled reminder editor payload. */ + seed: function (payload) { + let l = $inv.rd.layout(); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dopen'), data: { payload: JSON.stringify(payload) }, success: (r) => { + $inv.rd.tbl().data('rdtoken', r.token).data('rdver', r.version); + $fis.draft.bind(r.token, { + onReady: () => $inv.rd.refresh(), + onExpiring: (s) => $inv.rd.warnExpiry(s), + onClosed: (reason) => $inv.rd.closed(reason) + }); + $inv.rd.refresh(); + }, error: () => { l.rC('freeze'); } + }); + }, + /* Re-fetch the authoritative state and render the open-amount footer + validation from it. */ + refresh: function (cb) { + let t = $inv.rd.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('rem/dstate'), data: { token: t }, success: (state) => { + $inv.rd.applyState(state); if (typeof cb === 'function') { cb(state); } + }, error: (xhr) => { if (xhr && xhr.status === 410) { $inv.rd.closed('expired'); } }, + complete: () => { $inv.rd.layout().rC('freeze'); } + }); + }, + applyState: function (state) { + let tbl = $inv.rd.tbl(); if (tbl.length < 1) { return; } + /* See $inv.d.applyState — drop stale/out-of-order dstate responses so an older refresh + can never roll back a newer one's totals/validation. */ + let curVer = tbl.data('rdver'); + if (typeof curVer === 'number' && typeof state.version === 'number' && state.version < curVer) { + return; + } + tbl.data('rdver', state.version).data('serverSums', state.sums).data('remid', state.remid || ''); + $inv.rd.footer(tbl, state.sums || {}); + $inv.rd.notes(state.notes || {}); + $inv.rd.validation(state.validation || []); + }, + /* Renders the backend-authoritative intro/closing notice paragraphs (ADR: notice texts + are backend-authoritative) exactly as FuchsPdf.BuildReminderNotes emits them for the + PDF, so the online editor always shows precisely what will be printed. */ + notes: function (notes) { + let frm = $inv.rd.layout(); if (frm.length < 1) { return; } + let rq = frm.find('.srq'); if (rq.length < 1) { return; } + rq.children('.dnote').remove(); + let intro = (notes || {}).intro || [], first = rq.children().first(); + intro.slice().reverse().forEach((t) => { + let p = $$[0]('p').aC('dnote intro').rwText(t); + if (first.length) { p.insertBefore(first); } else { p.appendTo(rq); } + }); + ((notes || {}).closing || []).forEach((t) => $$[0]('p').aC('dnote closing').rwText(t).appendTo(rq)); + }, + /* Send one change to the server; the draftReady signal and this success both refresh. */ + sync: function (delta) { + let t = $inv.rd.token(); if (t === '') { return; } + $inv.rd.layout().aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dpatch'), data: { token: t, delta: JSON.stringify(delta) }, + success: () => { $inv.rd.refresh(); }, + error: (xhr) => { $inv.rd.layout().rC('freeze'); if (xhr && xhr.status === 410) { $inv.rd.closed('expired'); } } + }); + }, + /* Map an inline recipient field to its delta target and send it. */ + syncField: function (nme, val) { + if ($inv.rd.token() === '') { return; } + let map = { subject: 'subject', invoiceaddress: 'address', invoiceemail: 'email', text: 'text' }; + let target = map[nme]; if (!target) { return; } + $inv.rd.sync({ Target: target, Value: val }); + }, + /* Amount / amount-paid come from the item-row dialog; send both as their own deltas. */ + syncAmount: function (amount, amount_payed) { + if ($inv.rd.token() === '') { return; } + $inv.rd.sync({ Target: 'amount', Value: (amount != null ? amount : 0).toString() }); + $inv.rd.sync({ Target: 'amount_payed', Value: (amount_payed != null ? amount_payed : 0).toString() }); + }, + /* Render the open-amount footer from the server sums. Detail rows ($inv.rRemRw) have + 6 columns (aux, invoiceid, date, amount, amount_payed, open); span the label over the + 4 columns between aux and the open-amount column so the total lands in the same last + column as the per-invoice open amounts. */ + footer: function (tbl, sums) { + let ft = tbl.children('tfoot').empty(); + let tr = $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Offener Betrag')]); + $$.tdc('currency', tr, fnum(sums.amount_open || 0, $rct.cst)); + }, + validation: function (msgs) { + let frm = $inv.rd.layout(); if (frm.length < 1) { return; } + let box = frm.children('.dvalidation'); + if (box.length < 1) { box = $$.dc('dvalidation'); frm.prepend(box); } + box.empty().tC('hidden', (msgs || []).length < 1); + $.each(msgs || [], (i, m) => $$.dc('dvmsg', box).aC(m.severity).text(m.message)); + }, + /* PDF preview straight from the cache; confirm = flush + finalise + email, cancel = discard. */ + preview: function () { + let t = $inv.rd.token(); if (t === '') { return; } + let l = $inv.rd.layout(); + let email = (($inv.rd.tbl().data('new') || {}).invoiceemail) || ''; + if ($fis.ValidateEmail(email) === false) { if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { return; } } + l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dpreview'), data: { token: t }, success: (response) => { + l.rC('freeze'); + let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88); + $.each(response.img || [], (ii, img) => { $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); }); + $ocms.dlg(c, { + size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $ict.remd, + confirm: function (e) { + let ct = $(this); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dsave'), data: { token: t }, success: (sv) => { + $ocms.postXT({ + url: $ocms.url('rem/conf'), data: { id: sv.remid }, success: () => { + ct.trigger('modal_close'); + window.open($ocms.url('rem/idoc') + '?id=' + sv.remid, '_blank'); + $inv.rd.close(); + $ocms.init('req'); $inv.rReload(); + }, error: () => { alert($t.f1); ct.trigger('modal_close'); }, complete: () => { l.rC('freeze'); } + }); + }, error: () => { l.rC('freeze'); alert($t.f1); } + }); + }, + cancel: function (e) { if (confirm($ict.cdI)) { $inv.rd.close(); $inv.rReload(); } } + }); + }, error: () => { l.rC('freeze'); alert($t.f1); } + }); + }, + /* Zwischenspeichern: flush the cache to the DB (no re-upload); stay in the editor. */ + save: function () { + let t = $inv.rd.token(); if (t === '') { return; } + let l = $inv.rd.layout(); l.aC('freeze'); + $ocms.postXT({ + url: $ocms.url('rem/dsave'), data: { token: t }, success: (r) => { $inv.rd.tbl().data('remid', r.remid); }, + error: () => { alert($t.f1); }, complete: () => { l.rC('freeze'); } + }); + }, + history: function () { + let t = $inv.rd.token(); if (t === '') { return; } + $ocms.postXT({ + url: $ocms.url('rem/dhistory'), data: { token: t }, success: (r) => { + let c = $$.dc('dhist'); + if ((r.history || []).length < 1) { $$.dc('note', c).text('Noch keine Änderungen erfasst.'); } + else { + let ts = $$.tblset({ class: 'invtbl fullwidth' }, c); + $$.tr(ts.hd).append([$$.th().text('Zeit'), $$.th().text('Feld'), $$.th().text('Alt'), $$.th().text('Neu')]); + $.each(r.history, (i, h) => $$.tr(ts.bdy).append([$$.tdc('keep', fdt(h.timestamp)), $$.td().text(h.target), $$.td().text(h.oldValue), $$.td().text(h.newValue)])); + } + $ocms.dlg(c, { width: 800, form: false }); + } + }); + }, + warnExpiry: function (secondsLeft) { + let mins = Math.max(1, Math.round((secondsLeft || 0) / 60)); + $fis.notifications.push({ severity: 'info', title: 'Entwurf läuft ab', message: 'Der Mahnentwurf läuft in etwa ' + mins + ' Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren.' }); + }, + closed: function (reason) { + let t = $inv.rd.token(); + $inv.rd.tbl().removeData('rdtoken'); + if (t !== '') { $fis.draft.release(t); } + $fis.frm_edit().remove(); $fis.lf(true); + $fis.notifications.push({ severity: 'error', title: 'Entwurf geschlossen', message: reason === 'expired' ? 'Der Mahnentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.' : 'Der Mahnentwurf wurde geschlossen.' }); + try { $inv.rReload(); } catch (e) { } + }, + close: function () { + let t = $inv.rd.token(); + if (t !== '') { $ocms.postXT({ url: $ocms.url('rem/dclose'), data: { token: t } }); $fis.draft.release(t); } + $inv.rd.tbl().removeData('rdtoken'); + } +}; $inv.cInv2 = function (data) { let fr = $$.dc('rfrm').ldng(1); let o = $ocms.dlg(fr, { width: 1000 }); @@ -808,7 +1305,7 @@ $inv.ccInv = function (ev) { //normale rechnung rif.tbl.children('tbody').each($inv.bdysort); rif.tbl.trigger('fds.inv'); /* trigger calculations */ - $inv.eM(false, true, 'iss,p13b,setm,ctp'); + $inv.eM(false, true, $inv.emOpt(admin)); }, complete: () => { o.c.trigger('modal_close'); } @@ -953,7 +1450,7 @@ $inv.cntInv = function (data) { //invoice continuation rif.tbl.children('tbody').each($inv.bdysort); rif.tbl.trigger('fds.inv'); /* trigger calculations */ - $inv.eM(false, true, 'iss,p13b,setm,ctp'); + $inv.eM(false, true, $inv.emOpt(response.admin)); }, complete: () => { o.c.trigger('modal_close'); } @@ -1028,9 +1525,12 @@ $inv.cSt = function (data) { }; $inv.eHtml = function (ev) { let t = $(this), frmct = ev.data instanceof jQuery ? ev.data : ev.data.t; - /* invoiceemail must stay plain text — using the TinyMCE/html editor here used to wrap the - address in

tags, which then got posted and persisted verbatim into SendToEmail. */ - let isPlainText = ev.data.nme === 'invoiceemail'; + /* Single-line fields must stay plain text — the TinyMCE/html editor wraps the value in

+ tags, which used to get posted and persisted verbatim (e.g.

18.06.2026

in the + Leistungsdatum). The backend sanitises HTML too (single source of truth, ADR 0006), but + keeping these plain here avoids the UI briefly holding the wrapped value. Multi-line fields + (invoiceaddress, loc) stay HTML-capable and are normalised to newlines server-side. */ + let isPlainText = ['invoiceemail', 'provisionperiod', 'invoicetitle'].includes(ev.data.nme); let flds = isPlainText ? [{ name: 'txt', label: 'Text', type: 'text', value: frmct.text() }] : [{ name: 'txt', label: 'Text', type: 'html', value: frmct.html(), tinymce: true, attr: { style: 'height: 300px' } }]; @@ -1046,6 +1546,11 @@ $inv.eHtml = function (ev) { if (typeof change === 'function') { change(response.txt); } + /* backend-authoritative: mirror the inline recipient-field edit to the server session. + Invoice and reminder editors share this DOM; each syncField no-ops unless its own + draft token is present, so only the active mode's session receives the delta. */ + $inv.d.syncField(ev.data.nme, isPlainText ? (response.txt || '') : response.txt); + $inv.rd.syncField(ev.data.nme, isPlainText ? (response.txt || '') : response.txt); }, tinymce: { valid_elements: 'br', hidemenu: true, hidetoolbar: true } } @@ -1068,6 +1573,8 @@ $inv.eHtml = function (ev) { } $ocms.dlgform(flds, sets); }; +/* No client-side arithmetic (ADR 0006/0008): only the chosen VAT rate is stored on each row; + the server (InvoiceDraftCalculator.RecomputeLineValues) recomputes vat_val/svcvat_val from it. */ $inv.setVat = function (ev) { let t = $(this), thisrow = ev.data; let vat = prompt($rct.rqV); @@ -1080,12 +1587,6 @@ $inv.setVat = function (ev) { thisrow.siblings('.itm').each(function () { let rwi = $(this), dta = rwi.data(); dta.vat = fnum(vat, { style: 'percent' }).replace(' ', ''); - if ((dta.net_val || 0) > 0) { - dta.vat_val = dta.net_val * vat; - } - if ((dta.svcnet_val || 0) > 0) { - dta.svcvat_val = dta.svcnet_val * vat; - } }); $inv.t_fds_inv(); } @@ -1131,7 +1632,11 @@ $inv.eRw = function(row, dta, flds) { }, typedvalues: true }); }; -$inv.bdysort = (i, e) => { $(e).Sortable({ dragItem: false, dragHandleClass: 'ico', parentident: 'tr', swapdone: (p1, p2, i1, i2) => { $inv.t_fds_inv(); } }) } +/* Reorder items via drag. The DOM swap happens inside the Sortable during the drag; we commit + the new order once, reliably, on drop (onend) — that recomputes positions/totals and pushes the + changed block(s) to the backend session (t_fds_inv -> syncChanged). Committing on drop (rather + than on every mid-drag hover-swap) avoids rebuilding the row that is currently being dragged. */ +$inv.bdysort = (i, e) => { $(e).Sortable({ dragItem: false, dragHandleClass: 'ico', parentident: 'tr', onend: () => { $inv.t_fds_inv(); } }) } $inv.rrw = function () { let rw = $(this), dta = rw.data(), co = {}, ph = rw.is('.placeholder'), hn = rw.is('.hidenote'); let oHtml = (e) => $$.d().append(e).html(); @@ -1141,6 +1646,16 @@ $inv.rrw = function () { if (ph === false) { bc.unshift($$.dc('ibtn edit', { title: $rct.cP }).append(gi('pencil')).click(rw, $inv.eRow)); bc.push($$.dc('ibtn del', { title: $rct.dR }).append(gi('trash')).click(function (e) { if (confirm($rct.cD)) { rw.remove(); $inv.t_fds_inv(); } })); + /* "Auf Setpreis umstellen": offered on a Set header item that does not yet carry a + price. fds__prepInvoice's [SetItmId] now anchors on the still-unconverted (price 0) + header, so the header row's own SetItmId self-references its own id rather than being + null — the real signal for showing this button is still the header's own price/net_val + being 0 (once converted it carries a nonzero price and the button disappears). + Irreversible: the actual sum/clear mutation happens server-side (backend-authoritative, + see InvoiceDraftEditService.ApplyItemSetPrice) via the "item.setprice" patch target. */ + if ((dta.Type || '').toLowerCase() === 'set' && (dta.net_val || 0) === 0) { + bc.push($$.dc('ibtn tosetp', { title: $rct.toSetP }).append(gi('scale')).click(rw, $inv.toSetPrice)); + } } let axf = $$.dc('axf').append(bc); if (ph === true) { @@ -1148,7 +1663,7 @@ $inv.rrw = function () { } else if (rw.is('.itm.osum') === true) { co = { invrqid: dta.InvRqId, id: 'osum' + rw.index(), typ: 'osum', p: '', q: null, t: oHtml(dta.tbl.tbl), tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: false }; } else { - co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false }; + co = { invrqid: dta.InvRqId, id: dta.Id || '', typ: dta.Type || 'other', p: '', q: null, t: '', tt: null, v: null, vt: dta.net_val, vs: dta.svcnet_val, vat: dta.vat, vv: dta.vat_val, vsv: dta.svcvat_val, det: (dta.Note || '') !== '' && hn === false, SetItmId: dta.SetItmId || null }; $$.dc('ibtn ico move', axf, { title: $rct.mR }); /* should be the last added */ co.p = dta.position || (dta.SortOrder || ''); @@ -1158,7 +1673,7 @@ $inv.rrw = function () { co.t = dta.htmltext || (((dta.NameOrNumber || '').substr(0, 1) !== '#' ? oHtml($$[0]('p').text(dta.NameOrNumber)) : '') + (dta.Note || '')); } else { co.tt = co.det ? '' : $$.s(dta.Note || '').text(); - co.q = dta.quantity || (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')); + co.q = dta.quantity || ((dta.quantityhours || 0) !== 0 ? (fnum(dta.quantityhours) + ' ' + (dta.UnitString || '')) : ''); /* guard: no quantityhours -> blank, not fnum(undefined)="NaN" (e.g. the block set row) */ co.t = dta.htmltext ||(co.det ? (oHtml($$.s(dta.NameOrNumber || '')) + oHtml($$.dc('desc').html(dta.Note))) : oHtml($$.s(dta.NameOrNumber || ''))); co.v = dta.net co.vt = dta.net_val @@ -1177,45 +1692,62 @@ $inv.rrw = function () { tda.push($$.td(rw, { colspan: 4 }).append(co.t)); } else { Array.prototype.push.apply(tda, co.q ? [$$.tdc('keep').text(co.q)] : []); + /* ADR 0009: a null/undefined price renders an EMPTY cell (block-mode "Set mit Preis" + members, and the set row's own unit-price), distinct from a real 0,00 €. */ + let cur = (x) => (x == null ? '' : fnum(x, $rct.cst)); Array.prototype.push.apply(tda, [ $$.tdc('txt', { colspan: !co.q ? 2 : 1, title: co.tt }).append(co.t), - $$.tdc('currency').text(fnum(co.v, $rct.cst)), - $$.tdc('currency inetval').text(fnum(co.vt, $rct.cst)).attr('title', $rct.svcPart + ': ' + fnum(co.vs, $rct.cst)) + $$.tdc('currency').text(cur(co.v)), + $$.tdc('currency inetval').text(cur(co.vt)).attr('title', $rct.svcPart + ': ' + cur(co.vs)) ]); } - rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ).tC('hidenote', hn).append(tda); + rw.empty().attr('class', ph ? 'placeholder' : 'itm').aC(co.Typ) + .tC('sethdr', (co.typ || '').toString().toLowerCase() === 'set') /* emphasise set rows (fn1 header + block set row) */ + .tC('hidenote', hn).append(tda); dta.co = co; }; +/* "Auf Setpreis umstellen" (see $inv.rrw): backend-authoritative (ADR 0006) — the browser only + posts the target set-header item id; the server (InvoiceDraftEditService.ApplyItemSetPrice) + sums the member rows (same block, [SetItmId] equal to this row's id), writes that sum onto + the set header and clears each member's price, then the normal draftReady refresh re-renders + both the header and its members from the authoritative session state. This mirrors the PDF's + read of the same cached session, so the online editor and PDF can never drift apart. */ +$inv.toSetPrice = function (ev) { + let rw = ev.data, dta = rw.data(), id = (dta.Id || '').toString(); + if (id === '') { return; } + if (confirm($rct.toSetPc) === false) { return; } + /* Wait for the flush of any pending local edits to actually reach the server before sending + the conversion delta — sending both in parallel let a delayed block.replace (still carrying + the pre-conversion, zero member/header values) land after item.setprice and silently + overwrite the just-converted totals back to 0. */ + $.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: 'item.setprice', Ref: id }); }); +}; +/* No client-side totals/footer/tax computation (ADR 0006/0008): this only re-assembles each + block's row contract array (needed to post the "req" shape to the server) and, on the very + first pass, seeds the authoritative backend session. Footer, VAT breakdown, service-refund + note figures and the per-block sum cell are never computed here — they are rendered exclusively + from the server's `dstate` response by $inv.d.footer / $inv.d.applyState once the draft has a + token. Before a token exists the footer/notes/isum cells are simply left empty; there is no + local approximation to display. */ $inv.invSumUpdate = function () { - let tbl = $(this), ft = tbl.children('tfoot').empty(), p13b = bool((tbl.data().admin || {}).p13b || '', false); - tbl.nextAll('.fnote').remove(); - let sms = { ttn: 0, ttb: 0, ttvat: 0, tscn: 0, tscvat: 0, vat: {}, itmnet: {} }, ba = []; - let rwcy = (lbl, val, cls) => $$.tdc('currency', $$.tr(ft, { class: cls || 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text(lbl)]), fnum(val, $rct.cst)), fn = (t) => $$.dc('fnote').insertAfter(tbl).rwText(t); - let csms = function (rrx, sms, sid) { - sms.tscn += (rrx.svcnet_val || 0); - sms.tscvat += (rrx.svcvat_val || 0); - sms.ttn += (rrx.net_val || 0); - sms.ttvat += (rrx.vat_val || 0); - sms.ttb += ((rrx.net_val || 0) + (rrx.vat_val || 0)); - if ((rrx.vat || '') !== '') { - sms.vat[rrx.vat] = (sms.vat[rrx.vat] || 0) + (rrx.vat_val || 0); - } - //sms.itmnet[sid] = (sms.itmnet[sid] || 0) + (rrx.net_val || 0); - }; + let tbl = $(this); + let ba = []; let bds = tbl.children('tbody'); bds.each((bi, bdy) => { - let b = $(bdy), rx = b.data() || {}, i = [], citems = [], cset = null, bnet = 0, itm = b.find('tr.itm'), iso = 0, ipos = 0; + let b = $(bdy), rx = b.data() || {}, i = [], citems = [], itm = b.find('tr.itm'), iso = 0, ipos = 0; b.tC('empty', itm.length < 1); itm.each((ti, tx) => { - - let rrx = $(tx).data() || {}; csms(rrx, sms, rx.Id); bnet += (rrx.net_val || 0); i.push(rrx.co); - //console.debug('rrx %o', rrx); + let rrx = $(tx).data() || {}; + i.push(rrx.co); /* backend item contract (title/desc/qty/price_net/total_net + set flags), see InvoiceSetPricing. - Set grouping: an item of Type 'set' is a header that claims the following items in this - block as its members until the next set header (mfr__items has no explicit member link). */ + Set grouping: fds__prepInvoice computes [SetItmId] per item, anchored on the still-unconverted + (price 0) Set header that owns it — this is the authoritative membership signal (not row order), + so items not tagged with a SetItmId by the server are never swept into a preceding set. The + header row's own SetItmId self-references its own id, but it is never its own member (excluded + by the id !== '' && !== citem.id check below). */ let citem = $inv.itemToContract(rrx); - if (citem.type === 'set' && citem.id !== '') { cset = citem.id; } - else if (cset !== null && (citem.id || '') !== '') { citem.setId = cset; } + let sid = (rrx.SetItmId || '').toString(); + if (citem.type !== 'set' && sid !== '' && sid !== citem.id) { citem.setId = sid; } citems.push(citem); if (((typeof rrx.SortOrder === 'undefined' || rrx.SortOrder === null) ? -1 : rrx.SortOrder) > -1) { if (['text', 'title'].includes((rrx.Type || 'other').toLowerCase()) === false) { ipos++; } @@ -1224,42 +1756,18 @@ $inv.invSumUpdate = function () { $inv.rrw.call(tx); } }); - //console.debug('%o', { - // f: b.find('tr.isum > td.isumval'), t: fnum(bnet, $rct.cst), n: bnet - //}); - b.find('tr.isum > td.isumval').text(fnum(bnet, $rct.cst)); - ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems, netval: bnet }); + ba.push({ Id: rx.Id, nme: rx.Name, text: rx.text, itm: i, items: citems }); }); let nonempty = tbl.find('tbody:not(.empty)').length; bds.find('tr.isum').tC('hidden', nonempty < 2); - //let fnet = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Netto')]), fnum(sms.ttn, $rct.cst)); - rwcy('Netto', sms.ttn); - if (p13b === false) { - $.each(sms.vat, (vi, vx) => { - //$$.tdc('currency vat', $$.tr(ft, { class: 'tvat' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Umsatzsteuer ' + vi)]), fnum(vx, $rct.cst)); - rwcy($rct.vat + ' ' + vi, vx, 'tvat'); - }); - } else { - sms.ttb = sms.ttn; - } - //let fsum = $$.tdc('currency', $$.tr(ft, { class: 'tsum' }).append([$$.tdc('aux'), $$.td({ colspan: 4 }).text('Summe')]), fnum(sms.ttb, $rct.cst)); - rwcy('Summe', sms.ttb); - let itype = tbl.data().admin.type; - if (itype === 'i') { - fn($rct.note2); - fn($rct.note4); - } else if (itype === 'c') { - fn($rct.note2); - } else { - fn(string($rct.note3, [fnum((sms.tscn + sms.tscvat) * (tbl.data().admin.tax_servicerefund || 0), $rct.cst)])).aC('ntax'); - fn($rct.note2); - fn(string($rct.note1, [fnum(sms.tscn + sms.tscvat, $rct.cst), fnum(sms.tscn, $rct.cst), fnum(sms.tscvat, $rct.cst)])); - } - if (p13b === true) { - fn($rct.note13b); - } - tbl.data('sms', sms); tbl.data('bai', ba); + /* Backend-authoritative seeding (ADR 0006): on the first calculation of an invoice + draft (invSumUpdate is only bound for invoices, never reminders), hand the assembled + model to the server session and switch to server-driven totals from then on. */ + if ((tbl.data('dtoken') || '') === '' && bool(tbl.data('dseeding'), false) === false && ((tbl.data('admin') || {}).type != null)) { + tbl.data('dseeding', true); + $inv.d.seed($.extend($inv.invcPayload(tbl.data()), { invid: tbl.data('invid') || '' })); + } }; $inv.worknotes = function (rx) { let wn = ''; @@ -1347,7 +1855,21 @@ $inv.rendersrq = function () { $$.tdc('currency isumval', istr); } }; -$inv.t_fds_inv = () => { $('div.invoice_layout table.invi').trigger('fds.inv'); }; +$inv.t_fds_inv = () => { + let tbl = $('div.invoice_layout table.invi'); + tbl.trigger('fds.inv'); + /* After any local item mutation, push the changed block(s) to the authoritative + server session as granular deltas (invoice drafts only — reminders have no token, + nor a 'dseedpromise', so they correctly fall through to the no-op). While the initial + seed (inv/dopen) is still in flight there is no token yet either, but a 'dseedpromise' + is pending — wait for it instead of skipping the flush, otherwise an edit made in that + brief window is silently never sent. Returns the flush promise so callers that must + sequence a follow-up server delta after this flush (e.g. $inv.toSetPrice) can wait for + it instead of racing it. */ + if ((tbl.data('dtoken') || '') !== '') { return $inv.d.syncChanged(tbl); } + let seeding = tbl.data('dseedpromise'); + return seeding ? $.when(seeding).then(() => $inv.d.syncChanged(tbl)) : $.when(); +}; $inv.sedit = () => { $inv.sprev(true); }; @@ -1402,6 +1924,7 @@ $inv.sp13b = () => { } else { } tbl.trigger('fds.inv'); + $inv.d.sync({ Target: 'p13b', Value: d.admin.p13b }); }; /* Maps an item row's data to the backend item contract consumed by InvoiceSetPricing / FuchsPdf.ApplyInvoice: { id, type, title (plain), desc (html), qty, price_net, @@ -1412,7 +1935,10 @@ $inv.itemToContract = function (rrx) { rrx = rrx || {}; let oHtml = (e) => $$.d().append(e).html(); let type = (rrx.Type || '').toString().toLowerCase(); - let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: (rrx.net_val || 0), vat: rrx.vat || '' }; + /* ADR 0009: preserve null prices verbatim (null = empty cell, kept distinct from a real 0) so + block-mode nulled members and the set row round-trip unchanged when the block is re-posted. */ + let nz = (x) => (x == null ? null : x); + let ci = { id: (rrx.Id || '').toString(), type: type, title: '', desc: '', qty: '', price_net: '', total_net: nz(rrx.net_val), vat: rrx.vat || '' }; if (rrx.co && rrx.co.typ === 'osum') { /* combined single-sum line — the on-screen "title" is an HTML sub-table; render it as desc */ ci.desc = rrx.co.t || ''; @@ -1433,29 +1959,35 @@ $inv.itemToContract = function (rrx) { ci.desc = rrx.Note || ''; } ci.qty = rrx.quantity || ((rrx.quantityhours || 0) !== 0 ? (fnum(rrx.quantityhours) + (rrx.UnitString ? ' ' + rrx.UnitString : '')) : ''); - ci.price_net = (rrx.net || 0); - ci.total_net = (rrx.net_val || 0); + ci.price_net = nz(rrx.net); + ci.total_net = nz(rrx.net_val); } return ci; }; -/* 3-way set-pricing display switch. Mirrors §13b: writes the choice onto admin.setmode, - which BuildInvoiceParams turns into the "setmode:" InvoiceOptions token the PDF reads. */ +/* 2-way set-pricing display switch (SetPrice / SetOnly). Mirrors §13b: writes the choice onto + admin.setmode, which BuildInvoiceParams turns into the "setmode:" InvoiceOptions token + the PDF reads. ItemPrices ("Positionen mit Preis – Set als Überschrift") was removed: it was + always just the implicit pre-conversion default and is not a state the user can switch back to + once a set has been converted via the "Auf Setpreis umstellen" item switch (see $inv.toSetPrice / + InvoiceDraftEditService.ApplyItemSetPrice) — that conversion is one-way. */ $inv.ssetmode = () => { let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(); d.admin = d.admin || {}; let cur = (d.admin.setmode || 'setprice'), o; let btn = (mode) => $$.dc('btn', $ict.setmo[mode]).tC('selected', cur === mode).click(() => { o.c.trigger('modal_close'); $inv.setSetmode(mode); }); - let fr = $$.dc('choicefrm').append([btn('setprice'), btn('itemprices'), btn('setonly')]); + let fr = $$.dc('choicefrm').append([btn('setprice'), btn('setonly')]); o = $ocms.dlg(fr, { width: 800 }); }; +/* "Set mit Preis" / "Nur Set mit Preis" (ADR 0009): NOT a display toggle — an irreversible, + backend-authoritative mutation grouped by service-request block. The server inserts a dedicated + set row per block (block sum) and either nulls each member's price (setprice) or removes the + members (setonly); the normal draftReady refresh re-renders both via $inv.d.applyItems. The + browser only posts the chosen mode. Flush pending local edits first so a delayed block.replace + can't land after the conversion and overwrite it (same race guard as $inv.toSetPrice). */ $inv.setSetmode = (mode) => { - let l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(); - d.admin = d.admin || {}; - d.admin.setmode = mode; /* posted in admin -> BuildInvoiceParams writes setmode: into InvoiceOptions */ - d.inv = d.inv || {}; /* keep a local InvoiceOptions reflection in sync (cosmetic) */ - let opts = (d.inv.InvoiceOptions || '').split(',').filter(x => x !== '' && x.indexOf('setmode:') !== 0); - if (mode && mode !== 'setprice') { opts.push('setmode:' + mode); } - d.inv.InvoiceOptions = opts.join(','); + if (confirm($rct.toSetMc || $rct.toSetPc) === false) { return; } + let target = (mode === 'setonly') ? 'block.setonly' : 'block.setprice'; + $.when($inv.t_fds_inv()).done(() => { $inv.d.sync({ Target: target }); }); }; $inv.sctp = () => { let flds = $invcol.ctp; @@ -1472,126 +2004,45 @@ $inv.sctp = () => { cvo.contactEmail = response.email; d.new.CustomValues = JSON.stringify(cvo); //Assign it to new Values so that this is submitted also l.find('.ctpfrm').text(ne(response.name, response.email)); + $inv.d.sync({ Target: 'contact', Value: { name: response.name, email: response.email } }); }, typedvalues: true }); }; /* Normalises the editor's working model into the exact field names the C# backend - (FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload: - - balances/service sums come from `sms` (ttn/ttb), exposed on `new` as total_net/total_gross; - - every VAT rate's net amount is exposed as new.vat__net (the backend reads the highest); + (FdsInvoiceData.BuildInvoiceParams) reads, then returns the `invc` payload. No totals/VAT + are computed here (ADR 0006/0008: the online editor performs no arithmetic at all) — the + backend session computes total_net/total_gross/vat itself (InvoiceDraftCalculator) once + opened from this payload, and BuildFdsData/BuildInvoiceParams reads them from there: - new.invoicetitle -> new.title, new.loc -> new.provisionlocation, admin.paymentterms -> new.paymentterm, admin.CustomerId -> admin.customerid. Originals are kept alongside; the source objects are not mutated. */ $inv.invcPayload = function (d) { d = d || {}; - let sms = d.sms || {}, nw = $.extend({}, d.new), adm = $.extend({}, d.admin); - nw.total_net = sms.ttn || 0; - nw.total_gross = sms.ttb || 0; - /* VAT (rate + amount) is taken by the backend straight from the posted sms.vat map - (FdsInvoiceData.HighestVat), so no per-rate new.vat_* keys are needed here. */ + let nw = $.extend({}, d.new), adm = $.extend({}, d.admin); nw.title = (nw.invoicetitle != null ? nw.invoicetitle : (nw.title || '')); nw.provisionlocation = (nw.loc != null ? nw.loc : (nw.provisionlocation || '')); nw.paymentterm = (adm.paymentterms != null ? adm.paymentterms : (nw.paymentterm || '')); adm.customerid = (adm.customerid != null ? adm.customerid : adm.CustomerId); - return { admin: adm, req: d.bai, sms: d.sms, new: nw }; -}; -$inv.ssave = () => { - var l = $('div.invoice_layout'), d = l.find('table.invi').data(); - $inv.t_fds_inv(); - l.aC('freeze'); - $ocms.postXT({ - url: $ocms.url('req/save'), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid || '' }, success: (response) => { - $inv.cntInv({ id: response.id }); - }, error: () => { - alert($ict.eis); - }, complete: () => { - l.rC('freeze'); - } - }); -}; -$inv.sprev = (change) => { - var l = $('div.invoice_layout'), d = l.find('table.invi').data(); - change = bool(change, false); - $inv.t_fds_inv(); - l.aC('freeze'); - //console.debug({ admin: d.admin, req: d.bai, sms: d.sms, new: d.new }); - if ($fis.ValidateEmail(d.new.invoiceemail || '') === false) { - if (bool(confirm($ict.ivE + $ict.ivEc),false) === false) { - l.rC('freeze'); - return; - } - } - $ocms.postXT({ - url: $ocms.url('req/' + (change === true ?'sedit':'sprep')), data: { invc: JSON.stringify($inv.invcPayload(d)), id: d.invid ||'' }, success: (response) => { - let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), invid = response.id, invtp = response.total; - if (invtp > 10) { - $$.dc('note warn', c).text($ict.tpe); - } - $.each(response.img || [], function (ii, img) { - $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); - }); - for (let ic = (response.img || []).length + 1; ic <= invtp; ic++) { - $$.dc('pdfp ph', c).append($$.dc('note', $ict.pna)); - } - $ocms.dlg(c, { - size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $rct.crI, confirm: function (e) { - let ct = $(this); - l.aC('freeze'); /* spinner while the invoice is finalized/emailed on the backend */ - $ocms.postXT({ - url: $ocms.url('req/sconf'), data: { id: invid }, success: (cresp) => { - ct.trigger('modal_close'); - if (cresp.hasFile === true) { - window.open($ocms.url('req/idoc') + '?id=' + invid, '_blank'); /* open pdf in new tab, only if a file was actually created */ - } - $ocms.init('req'); /* go back to request list */ - $inv.rReload(); - }, error: () => { - alert($t.f1); - ct.trigger('modal_close'); - }, complete: () => { - l.rC('freeze'); - } - }); - }, cancel: function (e) { - let ct = $(this); - if (confirm($ict.cdI)) { - $ocms.postXT({ - url: $ocms.url('req/sdel'), data: { - id: invid - } - }); - } - $inv.rReload(); - } - }); - }, error: () => { - alert($ict.eis); - }, complete: () => { - l.rC('freeze'); - } - }); + return { admin: adm, req: d.bai, new: nw }; }; +/* Zwischenspeichern and preview now run against the backend-authoritative session + (ADR 0006): no full-invoice re-upload — the cached draft is flushed / rendered by + token. See $inv.d.save / $inv.d.preview. The finalise + email path (req/sconf) is + unchanged and is invoked from the preview modal's confirm handler. */ +$inv.ssave = () => { $inv.d.save(); }; +$inv.sprev = (change) => { $inv.d.preview(); }; $inv.rReload = () => { try { let s = $('#listframe ul.rql:first').data(); $inv.cInv2({ id: s.search }); } catch (e) { } }; -$inv.quantChange = function (i) { - //console.debug({ t: this, i: i }); - let t = $(this), f = t.closest('form'), fi = {}, pf = (i) => parseFloat(i.toString().replace('%', '').replace(',', '.')), rtp = (num) => num.toFixed(2); - f.find(':input').each((i, e) => { fi[$(e).attr('name')] = $(e); }); - let qv = parseInt(fi.quantityhours.val() || '0'), nv = pf(fi.net.val() || '0'), vat = pf(fi.vat.val()) * 0.01; - if (qv > 0 && nv > 0) { - fi.net_val.val(rtp(qv * nv)); - fi.vat_val.val(rtp(qv * nv * vat)); - if (['Service'].includes(fi.Type.val())) { - fi.svcnet_val.val(rtp(qv * nv)); - fi.svcvat_val.val(rtp(qv * nv * vat)); - } - } -}; +/* No client-side arithmetic (ADR 0006/0008): quantity/price/VAT edits are posted as raw values + and the server (InvoiceDraftCalculator.RecomputeLineValues) computes net_val/vat_val/ + svcnet_val/svcvat_val. The fields are left as entered here; the authoritative values are + pushed back onto the row by $inv.d.applyItems once the change reaches the backend. */ +$inv.quantChange = function (i) { }; $inv.storno = function (id, fds) { let o, fr = $$.dc('choicefrm').append([ $$.dc('btn', 'Storno ohne Details').click({ id: id, mode: 'simple' }, (ev) => { o.c.trigger('modal_close'); $inv.cSt(ev.data); }) @@ -1750,6 +2201,8 @@ $inv.eRowR = function (ev) { $.extend(tdta.rm, res); tbl.data(tdta); $inv.rRemRw.call(row, tdta); + /* backend-authoritative: mirror the edited amount / amount-paid to the server session */ + $inv.rd.syncAmount(tdta.rm.amount, tdta.rm.amount_payed); }, typedvalues: true }); }; @@ -1764,7 +2217,10 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation //console.debug(response); let rem = response.rm || {}; let rq = $$.dc('srq', frm); - $ict.remt[rem.type].forEach(r => $$[0]('p').rwText(r).appendTo(rq)); + /* Intro/closing notice paragraphs are backend-authoritative (see $inv.rd.notes, + populated from FuchsPdf.BuildReminderNotes via rem/dstate after seeding below) — + they are appended/prepended into this same .srq container once the draft session + is open, so no client-side wording is constructed here. */ let rif = $$.tblset({ class: 'invi' }, rq); rif.ft = $$[0]('tfoot'); rif.tbl.data($.extend({ invid: rem.invid, new: {} }, response)); @@ -1772,7 +2228,6 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation $ict.remHR.forEach(h => $$.th(thr, h)); $inv.rRemRw.call($$.tr(rif.bdy), rif.tbl.data()); rif.ft.appendTo(rif.tbl); - $ict.remt2[rem.type].forEach(r => $$[0]('p').rwText(r).appendTo(rq)); let fm = (cls, txt, nme, hd, dta) => { let fr = $$.dc('inpfrm', frm).aC(cls).append(typeof hd === 'string' ? $$.dc('ahd', hd) : (hd > 0 ? $$.dc('ahd', $rct.frm[nme]) : null)), fc = $$.dc('content', fr).rwText(txt); @@ -1790,57 +2245,26 @@ $inv.ccRem_s2 = function (id, sets) { //reminder creation rif.tbl.children('tbody').each($inv.bdysort); rif.tbl.trigger('fds.inv'); /* trigger calculations */ + /* Seed the authoritative server session (ADR 0006). Amounts join the recipient + fields in the 'new' block; the reference invoice data goes into 'rem'. From here + the backend owns the open-amount computation and validation; inline edits and the + item-row dialog post single deltas (see $inv.rd). */ + let nw = rif.tbl.data('new'); + nw.amount = rem.amount; nw.amount_payed = rem.amount_payed; + $inv.rd.seed({ + rem: { invid: rem.invid, type: rem.type, invoiceid: rem.invoiceid, invoicedate: rem.invoicedate }, + new: nw + }); }, complete: () => { //o.c.trigger('modal_close'); } }); }; $inv.rprev = () => { - var l = $('div.invoice_layout'), tbl = l.find('table.invi'), d = tbl.data(); - $.extend(d.new, tbl.find('tbody > tr:first').data()); - l.aC('freeze'); - //console.debug({ rem: d.rm, new: d.new }); - if ($fis.ValidateEmail(d.new.invoiceemail || '') === false) { - if (bool(confirm($ict.ivE + $ict.ivEc), false) === false) { - l.rC('freeze'); - return; - } - } - $ocms.postXT({ - url: $ocms.url('rem/prep'), data: { remc: JSON.stringify({ rem: d.rm, new: d.new }), id: d.invid || '' }, success: (response) => { - l.rC('freeze'); - let c = $$.dc('imagecollection pdfpreview'), vhr = Math.round(vh() * 0.88), remid = response.id; - $.each(response.img || [], function (ii, img) { - $$.dc('pdfp', c).append($$.img(img).css('max-height', (vhr - rpx(6)).toString() + 'px')); - }); - $ocms.dlg(c, { - size: [vhr, Math.round(vw() * 0.88)], zindex: 50, form: false, button: $ict.remd, confirm: function (e) { - let ct = $(this); - $ocms.postXT({ - url: $ocms.url('rem/conf'), data: { id: remid }, success: () => { - ct.trigger('modal_close'); - window.open($ocms.url('rem/idoc') + '?id=' + remid, '_blank'); /* open pdf in new tab */ - $ocms.init('req'); /* go back to request list */ - $inv.rReload(); - }, error: () => { - alert($t.f1); - ct.trigger('modal_close'); - } - }); - }, cancel: function (e) { - let ct = $(this); - if (confirm($ict.cdI)) { - $ocms.postXT({ - url: $ocms.url('rem/del'), data: { - id: remid - } - }); - } - $inv.rReload(); - } - }); - } - }); + /* Preview + finalise now run through the backend-authoritative session ($inv.rd): + the PDF renders straight from the server cache (no rem/prep DB write), and confirm + flushes (rem/dsave) then finalises + emails (rem/conf). */ + $inv.rd.preview(); }; $inv.sis = (id) => { if (confirm($ict.sisc)) { diff --git a/Fuchs/wwwroot/web/fis.inv.de.min.js b/Fuchs/wwwroot/web/fis.inv.de.min.js index 1d38dbc..ad06eed 100644 --- a/Fuchs/wwwroot/web/fis.inv.de.min.js +++ b/Fuchs/wwwroot/web/fis.inv.de.min.js @@ -1 +1 @@ -let $rct={mdl:"Aufträge",or:"offene Aufträge",orr:"offene Aufträge (4 W)",rn:"Auftragsnummer",iov:{all:"Auftragsübersicht (alle)","":"Auftragsübersicht"},wk:"Woche",nd:"Keine Daten gefunden.",h:"Uhr",rq1:"Auftragsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.",rq2:"Auftragsdaten werden geladen",rq1f:"Die Auftragsdaten von MFR konnten nicht oder nicht schnell genug abgerufen werde.\nMöchten Sie mit den bestehenden Daten trotzdem weitermachen?",note1:"Im Bruttobetrag sind {0} Lohnkosten enthalten (netto {1}). Die darin enthaltene Umsatzsteuer beträgt {2}.",note2:"Bitte beachten Sie, nach §14 Abs. 1 Umsatzsteuergesetz ist diese Rechnung ein Zahlungsbeleg oder eine andere beweiskräftige Unterlage für 2 Jahre nach Ablauf des Kalenderjahres der Ausstellung dieser Rechnung aufzubewahren, soweit nicht aufgrund anderer gesetzlicher Regelungen andere ggf.längere Aufbewahrungsfristen gelten.",note3:"Privathaushalten erstattet das Finanzamt bis zu {0} des Arbeitslohns mit der nächsten Steuererklärung.",note4:"Für bereits erbrachte Arbeiten, Dienstleistungen, Materiallieferungen und getätigte Bestellvorgänge zum oben genannten Bauvorhaben, die sich aus dem mit Ihnen geschlossenen Vertrag ergeben, stellen wir Ihnen vertragsgemäß unsere Akontozahlung in Rechnung. Eine Endabrechnung erhalten Sie als Schlussrechnung nach Abschluss des gesamten Bauvorhabens. Das Ausführungsdatum entnehmen Sie bitte dem Schlusstext dieser Rechnung. Wir danken Ihnen herzlich für das entgegengebrachte Vertrauen und bitten Sie um kurzfristigen Ausgleich der Akontorechnung.",note13b:"Gem. §13b Umsatzsteuergesetz unterliegen Sie der Steuerschuldnerschaft des Leistungsempfängers zur Umsatzsteuer aus dieser Rechnung mit einem Steuersatz von 19%.",crI:"Rechnung erstellen",crII:"Abschlagsrechnung erstellen",dII:"Für eine Abschlagsrechnung darf nur ein Auftrag gewählt werden.",dnS:"Für eine Rechnung muss mindestens ein Auftrag gewählt werden.",inv:"Rechnung",invs:"Rechnungen",req:"Auftrag",provP:"Leistungszeitraum",provD:"Leistungsdatum",cP:"Position ändern",iRb:"Zeile darunter einfügen",dR:"Zeile löschen",sV:"USt festlegen",cD:"Löschen?",mR:"Zeile verschieben",svcPart:"Service-Anteil",vat:"Umsatzsteuer",combP:"Positionen zusammenfassen",iSum:"Zwischensumme",dtRel:"Freigegeben am: ",dtCr:"Erstellt am: ",rqV:"USt des Auftrags?",cthd:"wirklich aus-/einblenden ?",cst:{style:"currency",currency:"EUR"},sts:{IsWorkDone:"Arbeiten erledigt",Closed:"Auftrag geschlossen",SubcontractorPendingConfirmation:"Warten auf Bestätigung (Unterauftrag)",Scheduled:"Geplant",OfferIsRejected:"Angebot abgelehnt",OfferIsSend:"Offen (Angebot versandt)",CollaborationWaitingConfirmation:"Warten auf Bestätigung (Zusammenarbeit)",Released:"Freigegeben",OfferIsConfirmed:"Bestätigt",InProgress:"In Bearbeitung",ReadyForScheduling:"Zur Planung",Created:"Erstellt",Rejected:"Abgebrochen",Invoiced:"Rechnung gestellt","-":"-"},invHR:["Pos.","Menge","Artikelbezeichnung","VK","Summe"],frm:{invoiceaddress:"Adresse",loc:"Leistungsort / Lieferadresse",invoiceemail:"Email"}},$rcol={req:new fields_definition("Auftrag","Aufträge",[{name:"tags",label:"",type:"string",dfnc:function(e,t){""!==(e||"")&&($(this).aC("tags"),e.split(",").forEach((e=>{""!==e&&$(this).append($$.sc("tag "+e.replace(" ","_").replace("/","_").toLowerCase(),e))})))}},{name:"DateOfCreation",label:"Datum",type:"date",title:function(e){$(this).attr("title",$rct.dtCr+fdt(e.DateOfCreation).ne("-")+" \n"+$rct.dtRel+fdt(e.DateReleased).ne("-"))}},{name:"CustomerName",label:"Kunde (Firma)",type:"string"},{name:"Name",label:"Auftragsname",type:"string"},{name:"ExternalId",label:"Auftragsnummer",type:"string"},{name:"ParentExtenalId",label:"PAuftrag",type:"string"},{name:"InvoiceId",label:"RNummer",type:"string",dfnc:function(e,t){$(this).rwText(e," ").find("span").each((function(){$(this).aC("cla").click({id:$(this).text()},$inv.jdbn)}))}},{name:"State",label:"Status",type:"string"},{name:"WorkDoneAt",label:"Erledigt am",type:"date"},{name:"Description",label:"Beschreibung",type:"html"}]),itm:new fields_definition("Auftragsposition","Auftragspositionen",[{name:"NameOrNumber",label:"Bezeichnung",type:"string"},{name:"Type",label:"Typ",type:"select",required:!0,value:"Text",url:[{value:"Text",label:"Text"},{value:"Equipment",label:"Ausrüstung"},{value:"Material",label:"Material"},{value:"Service",label:"Arbeitsleistung"}],change:function(e){$req.quantChange.call(this,e)}},{name:"quantityhours",label:"Anzahl / Menge",type:"number",precision:"0.01",value:1,change:function(e){$inv.quantChange.call(this,e)}},{name:"UnitString",label:"Einheit",type:"select",url:["LFDM","Stck","Std.","QM","AW","Pauschal"],change:function(e){$inv.quantChange.call(this,e)}},{name:"net",label:"EinzelPreis netto",type:"number",precision:"0.01",value:0,change:function(e){$inv.quantChange.call(this,e)}},{name:"net_val",label:"GesamtPreis netto",type:"number",precision:"0.01",value:0},{name:"vat_val",label:"GesamtPreis USt",type:"number",precision:"0.01",value:0},{name:"svcnet_val",label:"Arbeitslohn netto",type:"number",precision:"0.01",value:0},{name:"svcvat_val",label:"Arbeitslohn USt",type:"number",precision:"0.01",value:0},{name:"net_pos",label:"Netto",type:"string"},{name:"bo_pos",label:"Brutto",type:"string"},{name:"vat",label:"USt",type:"string",value:"19,0%",change:function(e){$inv.quantChange.call(this,e)}},{name:"Note",label:"Details",type:"html",tinymce:!0}])},$ict={mdl:"Rechnungen",iov:{all:"Rechnungen (alle)","":"Rechnungen (nur fertige)","#d":"Rechnungen (nur Entwürfe)","#u":"Rechnungen (nur unbezahlt)","#r":"Rechnungen (nur angemahnt)","#a":"Rechnungen (nur Akonto)","#c":"Rechnungen (nur Storno)","#ru":"Rechnungen (nur angemahnt + unbez.)"},uba:", gesamter Zeitraum)",req:"Auftrag",inv:"Rechnung",rem:"Mahnung",in:"Rechnungsnummer",cc:"Kunde",wk:"Woche",nd:"Keine Daten gefunden.",dl:"Herunterladen",ed:"Bearbeiten",ced:"Bearbeitung fortsetzen",sItm:"Einzelheiten anzeigen",sPay:"Zahlungen anzeigen",cdI:"Entwurf der Rechnung löschen?",rel:"Neu Laden",relm:"Bitte laden Sie Liste manuell neu, um die Änderungen zu sehen.",dsp:"Rechnung anzeigen",storno:"Storno-Rechnung erstellen",credit:"Gutschrift erstellen",remd:"Mahnung erstellen",remdt:"Mahnung erstellen zur Rechnung {0}",remlst:"Mahnungen anzeigen",remdsp:"Mahnung anzeigen",remres:"Mahnung erneut senden",remresc:"Mahnung {0} wirklich erneut senden?",remresr:"Mahnung {0} wurde erfolgreich versandt.",setpyd:"Bezahlt markieren",cpyd:"Rechnung wirklich als bezahlt markieren?",setupd:"Bezahlt-Markierung aufheben",cupd:"Bezahlt-Markierung wirklich aufheben?",ivE:"Die Email-Adresse ist vermutlich nicht gültig.",ivEc:"\nMöchten Sie fortfahren?",pna:"Diese Seite ist in der Vorschau nicht verfügbar",tpe:"Die Anzahl von {0} Seiten wird aktuell nicht unterstützt",eis:"Der Rechnungsentwurf konnte nicht gespeichert werden.",iss:"Zwischenstand speichern.",p13b:"USt -> §13b",setm:"Set-Preisanzeige",setmo:{setprice:"Set mit Preis – Positionen ohne Preis",itemprices:"Positionen mit Preis – Set als Überschrift",setonly:"Nur Set mit Preis – Positionen ausgeblendet"},ctp:"Ansprechpartner festlegen",mfr:"Von MFR neu abrufen",rq1:"Auftragsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.",rq2:"Auftragsdaten werden geladen",iq1:"Rechnungsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.",iq2:"Rechnungsdaten werden geladen",sis:"Rechnung als versandt markieren",srs:"Mahnung als versandt markieren",sisc:"Rechnung wirklich als versandt markieren?",srsc:"Mahnung wirklich als versandt markieren?",iSt:{dft:"Entwurf",uns:"nicht versandt",pyd:"bezahlt",cc:"storniert",op:"offen",due:"fällig",ovd:"überfällig",rem:"angemahnt"},rSt:["","Überfällig","2. Mahnung","3. Stufe"],pSt:{a:"Vollst.",p:"Teilz."},ivT:{i:"AbschlagsR.",f:"SchlussR",r:"Rechnung",c:"StornoR."},rovlh:"Übersicht der bisherigen Mahnungen",rovl:["Betreff","Betrag","Betrag gezahlt","fertiggestellt am"],remHR:["Rechnung","vom","Rechnungsbetrag","bereits bezahlt","noch offen"],remt:{f:["Sehr geehrte Damen und Herren,","ein Mahnschreiben sollte kurz, freundlich und erfolgreich sein. Kurz ist es, freundlich sowieso; ob es auch erfolgreich ist, hängt von Ihnen ab."],m:["Sehr geehrte Damen und Herren,","nun müssen wir Sie noch einmal anschreiben.","Wahrscheinlich haben Sie triftige Gründe dafür, warum Sie die Zahlung unserer Forderung nicht vornehmen und auch nicht auf unsere Mahnung reagieren. Sollten wir darüber nicht einmal sprechen?","Bitte nehmen Sie umgehend in dieser Sache mit uns Kontakt auf."],l:["Sehr geehrte Damen und Herren,",'Eine DRITTE MAHNUNG zu erhalten bereitet Ihnen bestimmt ebenso wenig Freude wie uns, sie zu verschicken. Leider haben wir auf unsere zweite Mahnung noch keine Antwort von Ihnen erhalten.", "Wir bitten Sie, den offenen Betrag innerhalb der nächsten 7 Werktage nach Erhalt dieses Schreibens zu begleichen. Nach Ablauf dieser Frist erfolgt keine weitere Mahnung mehr.',"Sollte die Forderung bis dahin nicht beglichen sein, eröffnen wir das gerichtliche Mahnverfahren. Sollten Sie die Rechnung inzwischen beglichen haben, so betrachten Sie bitte dieses Schreiben als gegenstandslos."]},remt2:{f:["Wir bitten Sie, den noch offenen Rechnungsbetrag innerhalb einer Woche auf unser Konto zu überweisen.","Sollten Sie den Betrag bereits überwiesen haben, so bitten wir Sie, diese Zahlungserinnerung als gegenstandslos zu betrachten."],m:["Um Ihnen zusätzliche Kosten für weitere Mahnungen zu ersparen, bitten wir Sie nunmehr um die Überweisung des noch zu zahlenden Gesamtbetrages inklusive der ggf. bereits fälligen Mahnzinsen und Mahngebühren innerhalb von einer Woche."],l:[]},payi:{account:"Konto",name:"Zahler",text:"Verw.Zweck",InvoiceID:"Rechnung",amount:"Betrag",date:"Datum",manual:"Typ"}},$invcol={datev:new fields_definition("Rechnung","Rechnungen",[{name:"Umsatz (ohne Soll/Haben-Kz)",label:"Umsatz (ohne Soll/Haben-Kz)",type:"string"},{name:"vf",label:"vf",type:"string"},{name:"Soll/Haben-Kennzeichen",label:"Soll/Haben-Kennzeichen",type:"string"},{name:"Konto",label:"Konto",type:"string"},{name:"Gegenkonto",label:"Gegenkonto",type:"string"},{name:"BU-Schlüssel",label:"BU-Schlüssel",type:"string"},{name:"Belegdatum",label:"Belegdatum",type:"string"},{name:"Belegfeld 1",label:"Belegfeld 1",type:"string"},{name:"Belegfeld 2",label:"Belegfeld 2",type:"string"},{name:"Buchungstext",label:"Buchungstext",type:"string"}]),inv:new fields_definition("Rechnung","Rechnungen",[{name:"invstatus",label:"Status",type:"select",url:$ict.iSt},{name:"balance",label:"Umsatz",type:"string",dtype:"currency"},{name:"CustomerName",label:"Kunde",type:"string"},{name:"InvoiceId",label:"RNummer",type:"string"},{name:"InvoiceType",label:"Typ",type:"select",url:$ict.ivT},{name:"request",label:"Auftrag",type:"string",dtype:"num"},{name:"vat",label:"MwSt",type:"string",dtype:"num"},{name:"deb_cred",label:"Soll/Haben",type:"string"},{name:"customer",label:"Konto",type:"string",dtype:"num"},{name:"contra_account",label:"Gegenkonto",type:"string",dtype:"num"},{name:"Belegdatum",label:"Belegdatum",type:"date"},{name:"reminderstatus",label:"MahnStatus",type:"select",url:$ict.rSt},{name:"reminder",label:"# Mahnungen",type:"integer"},{name:"Buchungstext",label:"Buchungstext",type:"string"},{name:"Payment",label:"Zahlung",type:"string"}]),rem:new fields_definition("Zahlungserinnerung","Zahlungserinnerung",[{name:"amount",label:"Rechnungsbetrag",type:"number",precision:"0.01",value:1},{name:"amount_payed",label:"bereits bezahlt",type:"number",precision:"0.01",value:1}]),rem2:new fields_definition("Zahlungserinnerung","Zahlungserinnerung",[{name:"DocumentName",label:"Name",type:"string"},{name:"subject",label:"Betreff",type:"string"},{name:"DateSent",label:"Versanddatum",type:"date"},{name:"status",label:"Status",type:"string"},{name:"amount_open",label:"offener Betrag",type:"number",precision:"0.01"},{name:"InvoiceId",label:"RNummer",type:"string"}]),rid:new fields_definition("Zahlungserinnerung","Zahlungserinnerung",[{name:"type",label:"Typ",type:"select",url:[["f","einfache Zahlungserinnerung"],["m","Mahnung"],["l","letzte Mahnung"]],required:!0},{name:"level",label:"Stufe",type:"select",url:[["1","Stufe 1"],["2","Stufe 2"],["3","Stufe 3"],["4","Stufe 4"],["5","Stufe 5"],["6","Stufe 6"]],required:!0}]),ctp:new fields_definition("Ansprechpartner","Ansprechpartner",[{name:"name",label:"Name",type:"string"},{name:"email",label:"Email",type:"string"}])},gi=(e,t)=>$$.sc("glyphicon glyphicon-"+e).aC(t),$inv={init2:function(e,t){e=e||"inv",t=t||{},$ocms.getScript([],(function(){$inv.init3(e,t)}))},init3:async function(e,t){$fis.cf(!0);let n=$fis.lf(!0);$("#topbar").ocmsmenu([]),$("#activemodule").text($ict.mdl);let i=[(async()=>{await $fis.getAuth("fds_inv")>0&&($inv.prepLst(""),n.aC("fix"))})(),new Promise(((e,t)=>{$fis.prepAuth(["fds_reminder"])}))];await Promise.all(i)},prepLst:function(e){let t=new Date,n=$fis.lf(!0).ldng(1),i=new Date("2021-01-01");$fis.frm_list().IN((function(){}));let a=[];$.each($ict.iov,((e,t)=>{a.push({lbl:t,fnc:()=>{$inv.prepLst(e),n.aC("fix")}})})),$fis.lfm().ocmsmenu([{lbl:"Filter",itm:a}]);$$.i({placeholder:$ict.in}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$inv.renderinv("i:"+n,"s","all"),t.val(""))})),$$.i({placeholder:$ict.cc}).appendTo($$.dc("mth ivc",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>=3&&(t.parent().aC("selected"),$inv.renderinv("c:"+n,"s","all"),t.val(""))}));"#"===e.substr(0,1)&&$$.dc("mth extra",n).text($ict.iov[e].replace(")",$ict.uba)).click((function(t){let n=$(this);if(t.stopPropagation(),n.siblings().rC("selected"),!0===n.is(".selected")){n.toggleClass("selected");let t=fdt(new Date,"yy-MM-dd");$inv.renderinv(t,"a",e)}n.aC("selected")})),n.append("
");let l=$$.dc("mthl",n),r=t.getFullYear(),s=t.getMonth()+1;for(let t=i.getFullYear();t<=r;t++){let n=$$.dc("yr").prependTo(l).text($ict.iov[e]+" - "+t.toString()).toggleClass("selected",t===r);n.click({yr:t},(function(e){e.stopPropagation(),n.siblings().rC("selected"),n.aC("selected")}));let a=$$.dc("mfrm",n);for(let n=0;n<(t!==r?12:s);n++){i=new Date(t,n,1);let l=$$.dc("mth").prependTo(a).text($ict.iov[e]+" - "+fdt(i,"MMM yyyy"));if(l.click({yr:t,mt:n},(function(t){if(t.stopPropagation(),l.siblings().rC("selected"),!0===l.is(".selected")){l.toggleClass("selected");let n=fdt(new Date(t.data.yr,t.data.mt,1),"yy-MM-dd");$inv.renderinv(n,"m",e)}l.aC("selected")})),""===e){$$.dc("mthdl",l).append(gi("compressed","ico")).click({yr:t,mt:n},(function(e){e.stopPropagation();let t=fdt(new Date(e.data.yr,e.data.mt,1),"yy-MM-dd");$inv.downloadzip(t,"m")}))}let r=getMonday(i),s=new Date(i);s.setMonth(s.getMonth()+1),s.setDate(0),s=getMonday(s);let c=$$.dc("wfrm",l);for(;r<=s;){let t=$$.dc("wk",c).text(($ict.wk||"W")+" "+fdt(r,"dd.MM.yy"));t.click({rd:new Date(r)},(function(n){n.stopPropagation();let i=fdt(n.data.rd,"yy-MM-dd");$inv.renderinv(i,"w",e),l.siblings().rC("selected").find(".wk").rC("selected"),l.aC("selected").find(".wk").rC("selected"),t.aC("selected")})),$$.dc("wkdl",t).append(gi("compressed","ico")).click({rd:new Date(r)},(function(e){e.stopPropagation();let t=fdt(e.data.rd,"yy-MM-dd");$inv.downloadzip(t,"w")})),r.setDate(r.getDate()+7)}}}n.ldng(0)},rerenderinv:function(){let e=$("#contentframe .invfrm:first");if(e.length>0){let t=e.data("sets")||{};t.mode&&$inv.renderinv(t.tgt,t.mode,t.includes)}},renderinv:function(e,t,n){let i=$fis.frm_list(!0,!0).ldng(1),a=$$.dc("invfrm",i).aC("md"+t).data("sets",$.extend({},{tgt:e,mode:t,includes:n})),l=$fis.lf();$ocms.postXT({url:$ocms.url("inv/invl"),data:{mode:t,tgt:e,includes:n},success:i=>{l.rC("fix").aC("hd"),$$.dc("ovhd",a).text(i.admin.title);let r=$$.tblset({},a),s=$invcol.inv,c=$$.tr(r.hd);$$.th(c);$.each(s.fields||[],((e,t)=>{$$.th(c).text(t.label),"vat"===t.name&&$$.th(c)})),$.each(i.invoices||[],((c,d)=>{let o=$$.tr(r.bdy);o.click((function(){l.rC("fix").aC("hd"),o.toggleClass("selected").siblings().rC("selected").find("td.av").rC("av"),o.find("td.av").rC("av"),!0===o.is(".selected")?$inv.iMn(d):$inv.eM()}));let m=$$.td(o,{class:"raux"});d.hasFile?($$.dc("idl ilbtn",m,{title:$ict.dl+"\n"+d.DocumentName}).append(gi("save-file","ico")).click({id:d.Id},$inv.downloadinv),$$.dc("idl ilbtn",m,{title:$ict.dsp+"\n"+d.DocumentName}).append(gi("eye-open","ico")).click({id:d.Id,typ:"inv"},$inv.jdisp)):!1===d.isFinal&&!0===$fis.isAuth("fds_inv",2)&&$$.dc("idl ilbtn",m,{title:$ict.ed}).append(gi("edit","ico")).click({id:d.Id},$inv.doContInv),$$.dc("iitm ilbtn",m,{title:$ict.sItm}).append(gi("list","ico")).click({id:d.Id},$inv.showitm),$$.dc("iitm ilbtn",m,{title:$ict.sPay}).append(gi("euro","ico")).click({id:d.Id},$inv.showpay),$.each(s.fields||[],((l,r)=>{let s,c,m=$$.td(o).aC(r.dtype);switch("select"===(r.type||"")?m.text((r.url||{})[d[r.name]]||""):m.text(d[r.name]),r.name||""){case"vat":s=$$.sel().appendTo($$.td(o,{class:"vsel"})),c=(i.admin.ust_options||"19,0%;16,0%;0,0%").split(";"),$.each(c,((e,t)=>{$$.opt(t,t).appendTo(s)})),s.click((function(e){e.stopPropagation()})).val(d[r.name]).change().change({frm:a,tgt:e,mode:t,id:d.Id,td:m,includes:n},$inv.setvat),m.toggleClass("hl",d[r.name].substr(0,2)!==c[0].substr(0,2)).click((function(e){e.stopPropagation(),$(this).toggleClass("av")}));break;case"balance":m.aC("sh_"+(d.SollHaben||"").toLowerCase());break;case"invstatus":case"reminderstatus":m.aC(("invstatus"===r.name?"is_":"rs_")+d[r.name])}}))}))},complete:()=>{i.ldng(0)}})},setvat:function(e){let t=$(this),n=e.data||{};$ocms.postXT({url:$ocms.url("inv/setvat"),data:{id:n.id,val:t.val()},success:e=>{n.td.rC("av"),$inv.renderinv(n.tgt,n.mode,n.includes)}})},downloadzip:function(e,t){$(this).empty();window.open($ocms.url("inv/datevzip?mode="+t+"&tgt="+encodeURIComponent(e)),"_blank")},showitm:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&$ocms.postXT({url:$ocms.url("inv/rqi"),data:{id:e.data.id},success:e=>{let t=$$.dc("rfrm");(e.requests||[]).length<1?t.text($ict.nd):$.each(e.requests||[],(function(e,n){let i=$$.dc("srq",t);$$.dc("nme",i).text(n.name);let a=$$.tblset({class:"if"},i);$.each(n.items||[],((e,t)=>{let n=$$.tr({id:"itm"+t.Id}).appendTo(a.bdy);$$.td(n).text(t.NameOrNumber),$$.td(n).text(t.Type),$$.td(n).aC("currency").text(t.net_pos),$$.td(n).aC("currency").text(t.bo_pos),$$.td(n).aC("num").text(t.vat)}))})),$ocms.dlg(t,{width:1e3})}})},showpay:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&$ocms.postXT({url:$ocms.url("inv/pyi"),data:{id:e.data.id},success:e=>{let t=$$.dc("rfrm");if((e.payments||[]).length<1)t.text($ict.nd);else{let n=$$.tblset({class:"if"},t),i=$$.tr(n.hd);$.each(["date","account","name","text","InvoiceID","amount","manual"],((e,t)=>{$$.th(i,$ict.payi[t])})),$.each(e.payments,((e,t)=>{let i=$$.tr({id:"itm"+t.banking_uid}).appendTo(n.bdy);$$.td(i).aC("date").text(t.date),$$.td(i).text(t.account),$$.td(i).text(t.name),$$.td(i).text(t.text),$$.td(i).text(t.InvoiceID),$$.td(i).aC("currency").text(t.amount),$$.td(i).text(t.manual)}))}$ocms.dlg(t,{width:1e3,title:"Übersicht der Zahlungen"})}})},downloadinv:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&window.open($ocms.url("inv/rdoc?id="+e.data.id),"_blank")},doContInv:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&$inv.cntInv({id:e.data.id})}},$$inv={init2:$inv.init2,auth:{}};export default $$inv;$inv.cInv=function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&!1!==$fis.isAuth("fds_inv",2)&&$inv.cInv2({id:e.data.id})},$inv.rMn=e=>{let t=[{lbl:$ict.req,itm:[]}];return!0===bool(e,!1)&&!0===$fis.isAuth("fds_inv",2)&&Array.prototype.push.apply(t[0].itm,[{lbl:$rct.crI,fnc:$inv.ccInv,data:{typ:"r"}},{lbl:$rct.crII,fnc:$inv.ccInv,data:{typ:"i"}}]),t.push({lbl:$ict.rel,fnc:$inv.rReload}),$("#topbar").ocmsmenu(t)},$inv.iMnr=e=>{let t=booln(e.isFinal,!0),n=e.Id,i=booln(e.fds,!1),a=[{glyph:"glyphicon-menu-left",fnc:()=>{$fis.frm_edit().remove()}},{lbl:$ict.inv,itm:[]},{lbl:$ict.rem,itm:[]}];return!1===t&&!0===$fis.isAuth("fds_inv",2)?a[1].itm.push({lbl:$ict.ced,fnc:$inv.clCntInv}):!0===$fis.isAuth("fds_inv",1)&&a[1].itm.push({lbl:$ict.dsp,fnc:()=>$inv.disp(n,"inv")}),!0===i&&!0===t&&!0===$fis.isAuth("fds_inv",2)&&(a[1].itm.push({lbl:$ict.storno,fnc:()=>$inv.storno(n,i)}),a[1].itm.push({lbl:$ict.credit,fnc:()=>$inv.credit(n,i)})),!0===t&&!0===$fis.isAuth("fds_reminder",2)&&(a[2].itm.push({lbl:$ict.remd,fnc:()=>$inv.ccRem(n,e.InvoiceId)}),a[2].itm.push({lbl:$ict.remlst,fnc:()=>$inv.dspRem(n)})),!0===t&&!0===$fis.isAuth("fds_reminder",2)&&!1===booln(e.IsSent,!1)&&a[2].itm.push({lbl:$ict.srs,fnc:()=>$inv.srs(n)}),a.push({lbl:$ict.rel,fnc:$inv.rReload}),$("#topbar").ocmsmenu(a)},$inv.iMn=e=>{let t=booln(e.isFinal,!0),n=e.Id,i=booln(e.fds,!1),a=[{glyph:"glyphicon-menu-left",fnc:()=>{$fis.frm_edit().remove()}},{lbl:$ict.inv,itm:[]},{lbl:$ict.rem,itm:[]}];return!1===t&&!0===$fis.isAuth("fds_inv",2)?a[1].itm.push({lbl:$ict.ced,fnc:()=>{$inv.cntInv({id:n})}}):!0===$fis.isAuth("fds_inv",1)&&a[1].itm.push({lbl:$ict.dsp,fnc:()=>$inv.disp(n,"inv")}),!0===$fis.isAuth("fds_inv",2)&&(a[1].itm.push({lbl:$ict.storno,fnc:()=>$inv.storno(n,i)}),a[1].itm.push({lbl:$ict.credit,fnc:()=>$inv.credit(n,i)})),!0===t&&!1===booln(e.IsPayed,!1)?(!0===$fis.isAuth("fds_reminder",2)&&a[2].itm.push({lbl:$ict.remd,fnc:()=>$inv.ccRem(n,e.InvoiceId)}),!0===$fis.isAuth("fds_inv",2)&&a[1].itm.push({lbl:$ict.setpyd,fnc:()=>$inv.setPyd(n)})):!0===t&&!0===booln(e.IsPayed,!1)&&"m"===(e.PaymentStatus||"")&&!0===$fis.isAuth("fds_inv",2)&&a[1].itm.push({lbl:$ict.setupd,fnc:()=>$inv.setUpd(n)}),!0===$fis.isAuth("fds_reminder",2)&&a[2].itm.push({lbl:$ict.remlst,fnc:()=>$inv.dspRem(n)}),!0===t&&!0===$fis.isAuth("fds_inv",2)&&!1===booln(e.IsSent,!1)&&a[1].itm.push({lbl:$ict.sis,fnc:()=>$inv.sis(n)}),!1===i&&a[1].itm.push({lbl:$ict.mfr,fnc:()=>$inv.mfrrel(n)}),$("#topbar").ocmsmenu(a)},$inv.eM=(e,t,n)=>{let i=[];return!0!==booln(e,!1)&&!0!==booln(t,!1)||i.push({glyph:"glyphicon-menu-left",fnc:()=>{$fis.lf(!0),$fis.frm_edit().remove()}}),!0===(n||"").split(",").includes("iss")&&i.push({lbl:$ict.iss,fnc:$inv.ssave}),!0===(n||"").split(",").includes("ctp")&&i.push({lbl:$ict.ctp,fnc:$inv.sctp}),!0===(n||"").split(",").includes("p13b")&&i.push({lbl:$ict.p13b,fnc:$inv.sp13b}),!0===(n||"").split(",").includes("setm")&&i.push({lbl:$ict.setm,fnc:$inv.ssetmode}),!0===booln(e,!1)&&i.push({lbl:$ict.rel,fnc:$inv.rReload}),$("#topbar").ocmsmenu(i)},$inv.cInv2=function(e){let t=$$.dc("rfrm").ldng(1),n=$ocms.dlg(t,{width:1e3});n&&n.ft.rwText($rct.rq1);let i=()=>{$ocms.postXT({url:$ocms.url("req/get"),timeout:60,data:{id:e.id,mode:"r"},success:t=>{t.admin=t.admin||{};let n=$fis.lf(!0).aC("fix").rC("hd");if($fis.frm_edit().IN(),$inv.eM(!0,!0),(t.requests||[]).length<1)n.aC("fix").text($rct.nd);else{$$.dc("lh",n,$rct.mdl);let i=$$.d(),a=$$.ul({class:"rql"}).data({search:e.id,parent:t.admin.parent}).appendTo(n),l={},r=$rcol.req.lbl();$.each(t.requests||[],(function(e,t){let n=$$.li({class:"cli rli"}).data($.extend({},t)).appendTo(a),s=$$.dc("lihd",n).addClass(t.state);!0===booln(t.open,!1)&&s.append($$.sc("cbox").click((()=>{n.tC("checked"),i.find("li").rC("checked"),!0===n.is(".checked")?$inv.rMn(t.open):$inv.eM(!0)}))),s.append([$$.sc("eid",t.ExternalId),$$.sc("nme",t.Name)]),$$.dc("lidt",n).append([$$.dc("rqs").append([$$.s(r.State+": "),$$.s($rct.sts[t.State||"-"])]),$$.dc("ivn").append([$$.s(r.InvoiceId+": "),$$.s(t.InvoiceId||"- -")]),$$.dc("wda").append([$$.s(r.WorkDoneAt+": "),$$.s(fdt(t.WorkDoneAt,"dd.MM.yyyy"))])]),l[t.Id]=n})),(t.inv||[]).length>0&&($$.dc("lh",n,$rct.invs),i=$$.ul({class:"ivl"}).appendTo(n),$.each(t.inv||[],((e,t)=>{let n=$$.li({class:"cli ili"}).data($.extend({},t)).appendTo(i),l=$$.dc("lihd",n).addClass(t.invstatus);!1===booln(t.isFinal,!0)?l.append($$.sc("cbox").click((()=>{""!==(t.Id||"")&&(n.tC("checked").siblings().rC("checked"),a.find("li").rC("checked"),!0===n.is(".checked")?$inv.iMnr(t):$inv.eM(!0))}))):["","dft"].indexOf(t.invstatus)<0&&l.append($$.sc("dli").click((function(){$inv.disp(t.Id,"inv")}))),l.append($$.sc("nme",t.DocumentName||t.Id)),$$.dc("lidt",n).append([$$.dc("wda").append([$$.s(fdt(t.DateCreated,"dd.MM.yyyy"))]),$$.d().text($ict.iSt[t.invstatus]||t.invstatus)])})))}},complete:()=>{n&&n.c.trigger("modal_close")}})};$ocms.postXT({url:$ocms.url("req/pget"),timeout:90,data:{id:e.id},success:e=>{n&&n.ft.rwText($rct.rq2),i()},error:()=>{confirm($rct.rq1f)?(n&&n.ft.rwText($rct.rq2),i()):n&&n.c.trigger("modal_close")}})},$inv.ccInv=function(e){let t=(e.data||{}).typ||"r",n=$fis.lf(),i=n.children("ul.rql"),a=i.data("parent"),l=[];if(i.find("li.rli.checked").each((function(){l.push($(this).data("Id"))})),l.length<1)return void alert($rct.dnS);if("i"===t&&l.length>1)return void alert($rct.dII);let r=$fis.frm_edit(),s=$$.dc("invoice_layout",r).append($$.dc("btn sprev").click($inv.sprev)),c=$fis.cf().width()>s.width()+n.width()+20;n.tC("fix",c).tC("hd",!c),$inv.eM(!1,!0);let d=$$.dc("rfrm").ldng(1),o=$ocms.dlg(d,{width:1e3});o.ft.rwText($rct.rq2),$ocms.postXT({url:$ocms.url("req/iget"),timeout:60,data:{id:a,mode:"ful",typ:t,sel:l.join(",")},success:e=>{let t=$$.dc("srq",s),n=$$.tblset({class:"invi"},t);n.bdy.remove(),n.ft=$$[0]("tfoot"),e.admin=e.admin||{},e.admin.p13b=bool(e.admin.p13b||"",!0===((e.inv||{}).InvoiceOptions||"").split(",").includes("§13b")),n.tbl.data($.extend({new:{},sms:{},itm:{}},{admin:e.admin,companies:e.companies,locations:e.locations}));let i=$$.tr(n.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(i,e))),n.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.requests||[],(function(t,i){if(0!==(i.Id||0)){let t=$inv.worknotes(i);i.text="i"===e.admin.type?$rct.req+jine([i.ExternalId,i.Name],": ").eine(" ",""):jine([jine([fdt(i.WorkDoneAt,"dd.MM.yy"),i.ExternalId]," - "+$rct.req+" "),t.ne(i.Name)],": \n");let a=$$.tbody(n.tbl).data($.extend({},i));$inv.rendersrq.call(a)}}));let a=$$.tr($$.tbody(n.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(a),n.ft.appendTo(n.tbl);let l,r,c=e.admin||{},d=(e,t,i,a,l)=>{let r=$$.dc("inpfrm",s).aC(e).append("string"==typeof a?$$.dc("ahd",a):a>0?$$.dc("ahd",$rct.frm[i]):null),c=$$.dc("content",r).rwText(t);$$.dc("axf",r).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:c,nme:i,change:e=>{n.tbl.data("new")[i]=e}},l),$inv.eHtml)),n.tbl.data("new")[i]=t};d("tfrm",c.invoicetitle,"invoicetitle",0,null),d("adrfrm",c.invoiceaddress,"invoiceaddress",0,null),d("locfrm","","loc",1,{list:deepCopy(e.locations),lbl:"ref",property:"address"}),d("emailfrm",c.invoiceemail,"invoiceemail",0,null),$$.dc("sndfrm",s).append($$.dc("content").text(c.sender)),c.provisionend&&(r=c.provisionstart?$rct.provP:$rct.provD,l=c.provisionstart?fdt(c.provisionstart,"dd.MM.yyyy")+" - "+fdt(c.provisionend,"dd.MM.yyyy"):fdt(c.provisionend,"dd.MM.yyyy")),d("admfrm",l,"provisionperiod",r,1),n.tbl.data("new").CustomValues=c.CustomValues||"",$$.dc("inpfrm ctpfrm",s).text(jObj(c.CustomValues,"contactName")),n.tbl.children("tbody").each($inv.bdysort),n.tbl.trigger("fds.inv"),$inv.eM(!1,!0,"iss,p13b,setm,ctp")},complete:()=>{o.c.trigger("modal_close")}})},$inv.ccStInv=function(e){let t=e.data||{},n=$fis.lf(),i=t.id,a=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.sprev)),l=$fis.cf().width()>a.width()+n.width()+20;n.tC("fix",l).tC("hd",!l),$inv.eM(!1,!0);let r=$$.dc("rfrm").ldng(1),s=$ocms.dlg(r,{width:1e3});s.ft.rwText($ict.iq1),$ocms.postXT({url:$ocms.url("inv/pget"),timeout:90,data:{id:t.id},success:e=>{s&&s.ft.rwText($ict.iq2),$ocms.postXT({url:$ocms.url("inv/icget"),timeout:60,data:{id:i},success:e=>{let t=$$.dc("srq",a),n=$$.tblset({class:"invi"},t);n.bdy.remove(),n.ft=$$[0]("tfoot"),e.admin=e.admin||{},e.admin.p13b=bool(e.admin.p13b||"",!0===((e.inv||{}).InvoiceOptions||"").split(",").includes("§13b")),n.tbl.data($.extend({new:{},sms:{},itm:{}},{admin:e.admin,companies:e.companies,locations:e.locations}));let i=$$.tr(n.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(i,e))),n.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.requests||[],(function(t,i){if(0!==(i.Id||0)){let t=$inv.worknotes(i);i.text="i"===e.admin.type?$rct.req+jine([i.ExternalId,i.Name],": ").eine(" ",""):jine([fdt(i.WorkDoneAt,"dd.MM.yy")+t.ne(i.Name)],": ");let a=$$.tbody(n.tbl).data($.extend({},i));$inv.rendersrq.call(a)}}));let l=$$.tr($$.tbody(n.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(l),n.ft.appendTo(n.tbl);let r,s,c=e.admin||{},d=(e,t,i,l,r)=>{let s=$$.dc("inpfrm",a).aC(e).append("string"==typeof l?$$.dc("ahd",l):l>0?$$.dc("ahd",$rct.frm[i]):null),c=$$.dc("content",s).rwText(t);$$.dc("axf",s).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:c,nme:i,change:e=>{n.tbl.data("new")[i]=e}},r),$inv.eHtml)),n.tbl.data("new")[i]=t};d("tfrm",c.invoicetitle,"invoicetitle",0,null),d("adrfrm",c.invoiceaddress,"invoiceaddress",0,null),d("locfrm",c.provisionlocation,"loc",1,{list:deepCopy(e.locations),lbl:"ref",property:"address"}),d("emailfrm",c.invoiceemail,"invoiceemail",0,null),$$.dc("sndfrm",a).append($$.dc("content").text(c.sender)),c.provisionend&&(s=c.provisionstart?$rct.provP:$rct.provD,r=c.provisionstart?fdt(c.provisionstart,"dd.MM.yyyy")+" - "+fdt(c.provisionend,"dd.MM.yyyy"):fdt(c.provisionend,"dd.MM.yyyy")),d("admfrm",r,"provisionperiod",s,1),n.tbl.data("new").CustomValues=c.CustomValues||"",$$.dc("inpfrm ctpfrm",a).text(jObj(c.CustomValues,"contactName")),n.tbl.children("tbody").each($inv.bdysort),n.tbl.trigger("fds.inv")},complete:()=>{s.c.trigger("modal_close")}})},error:()=>{s&&s.c.trigger("modal_close")}})},$inv.clCntInv=function(e){let t=$fis.lf(!1),n=[];t.find("li.ili.checked").each((function(){n.push($(this).data("Id"))})),1===n.length&&$inv.cntInv({id:n[0]})},$inv.cntInv=function(e){e=e||{};$fis.lf(!1).rC("fix").aC("hd");let t=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.sedit));$inv.eM(!1,!0);let n=$$.dc("rfrm").ldng(1),i=$ocms.dlg(n,{width:1e3});i.ft.rwText($rct.rq2),$ocms.postXT({url:$ocms.url("inv/get"),timeout:60,data:{id:e.id},success:e=>{e.admin=e.admin||{};let n=e.inv||{},i=$$.dc("srq",t),a=$$.tblset({class:"invi"},i);a.bdy.remove(),a.ft=$$[0]("tfoot"),a.tbl.data($.extend({invid:n.Id,new:{},sms:{},itm:{},bai:[]},e));let l=$$.tr(a.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(l,e))),a.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.req||[],(function(e,t){let n=$$.tbody(a.tbl).data($.extend({},t));$inv.rendersrq.call(n)}));let r=$$.tr($$.tbody(a.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(r),a.ft.appendTo(a.tbl);let s=(e,n,i,l,r)=>{let s=$$.dc("inpfrm",t).aC(e).append("string"==typeof l?$$.dc("ahd",l):l>0?$$.dc("ahd",$rct.frm[i]):null),c=$$.dc("content",s).rwText(n);$$.dc("axf",s).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:c,nme:i,change:e=>{a.tbl.data("new")[i]=e}},r),$inv.eHtml)),a.tbl.data("new")[i]=n};s("tfrm",n.InvoiceTitle,"invoicetitle",0,null),s("adrfrm",n.SendToAddress,"invoiceaddress",0,null),s("locfrm",n.ProvisionLocation,"loc",1,null),s("emailfrm",n.SendToEmail,"invoiceemail",0,null),$$.dc("sndfrm",t).append($$.dc("content").text(e.admin.sender)),s("admfrm",n.ProvisionPeriod,"provisionperiod",!0===(n.ProvisionPeriod||"").includes("-")?$rct.provP:$rct.provD,1),a.tbl.data("new").CustomValues=n.CustomValues||"",$$.dc("inpfrm ctpfrm",t).text(jObj(n.CustomValues,"contactName")),a.tbl.children("tbody").each($inv.bdysort),a.tbl.trigger("fds.inv"),$inv.eM(!1,!0,"iss,p13b,setm,ctp")},complete:()=>{i.c.trigger("modal_close")}})},$inv.cSt=function(e){e=e||{};let t=$fis.lf(),n=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.sedit)),i=$fis.cf().width()>n.width()+t.width()+20;t.tC("fix",i).tC("hd",!i),$inv.eM(!1,!0);let a=$$.dc("rfrm").ldng(1),l=$ocms.dlg(a,{width:1e3});l.ft.rwText($ict.iq1),$ocms.postXT({url:$ocms.url("inv/pget"),data:{id:e.id},success:t=>{l&&l.ft.rwText($ict.iq2),$ocms.postXT({url:$ocms.url("inv/storno"),data:{id:e.id,mode:e.mode},success:e=>{e.admin=e.admin||{},e.admin.p13b=bool(e.admin.p13b||"",!0===((e.inv||{}).InvoiceOptions||"").split(",").includes("§13b"));let t=e.inv||{},i=$$.dc("srq",n),a=$$.tblset({class:"invi"},i);a.bdy.remove(),a.ft=$$[0]("tfoot"),a.tbl.data($.extend({invid:t.Id,new:{},sms:{},itm:{},bai:[]},e));let l=$$.tr(a.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(l,e))),a.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.req||[],(function(e,t){let n=$$.tbody(a.tbl).data($.extend({},t));$inv.rendersrq.call(n)}));let r=$$.tr($$.tbody(a.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(r),a.ft.appendTo(a.tbl);let s=(e,t,i,l,r)=>{let s=$$.dc("inpfrm",n).aC(e).append("string"==typeof l?$$.dc("ahd",l):l>0?$$.dc("ahd",$rct.frm[i]):null),c=$$.dc("content",s).rwText(t);$$.dc("axf",s).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:c,nme:i,change:e=>{a.tbl.data("new")[i]=e}},r),$inv.eHtml)),a.tbl.data("new")[i]=t};s("tfrm",t.InvoiceTitle,"invoicetitle",0,null),s("adrfrm",t.SendToAddress,"invoiceaddress",0,null),s("locfrm",t.ProvisionLocation,"loc",1,null),s("emailfrm",t.SendToEmail,"invoiceemail",0,null),$$.dc("sndfrm",n).append($$.dc("content").text(e.admin.sender)),s("admfrm",t.ProvisionPeriod,"provisionperiod",!0===(t.ProvisionPeriod||"").includes("-")?$rct.provP:$rct.provD,1),a.tbl.data("new").CustomValues=t.CustomValues||"",$$.dc("inpfrm ctpfrm",n).text(jObj(t.CustomValues,"contactName")),a.tbl.children("tbody").each($inv.bdysort),a.tbl.trigger("fds.inv")},complete:()=>{l.c.trigger("modal_close")}})},error:()=>{l&&l.c.trigger("modal_close")}})},$inv.eHtml=function(e){let t=$(this),n=e.data instanceof jQuery?e.data:e.data.t,i="invoiceemail"===e.data.nme,a=i?[{name:"txt",label:"Text",type:"text",value:n.text()}]:[{name:"txt",label:"Text",type:"html",value:n.html(),tinymce:!0,attr:{style:"height: 300px"}}],l=e.data.change||null,r={title:t.data("dialog")||"",success:function(e){i?n.text(e.txt||""):n.html(e.txt),"function"==typeof l&&l(e.txt)},tinymce:{valid_elements:"br",hidemenu:!0,hidetoolbar:!0}};if(Array.isArray(e.data.list)){let t=$$.dc("lstfrm");$.each(e.data.list,((n,i)=>{let a=$$.dc("li",t).append(""!==(e.data.lbl||"")?$$.dc("lbl").rwText(i[e.data.lbl]):null);$$.dc("adr",a).rwText(i[e.data.property]).data("val",i[e.data.property]).click((function(){let e=$(this),t=e.closest(".modal-body").find(':input[name="txt"]');t.is(".tinymce")?tinymce.get(t.attr("id")).setContent($$.s().rwText(e.data("val")).html()):"TEXTAREA"===t.prop("tagName")?t.val(e.data("val")).change():t.rwText(e.data("val"))}))})),r.addcontent=t}$ocms.dlgform(a,r)},$inv.setVat=function(e){$(this);let t=e.data,n=prompt($rct.rqV);n&&(n=parseFloat(n.replace("%","")),n>1&&(n*=.01),!1===isNaN(n)&&(t.siblings(".itm").each((function(){let e=$(this).data();e.vat=fnum(n,{style:"percent"}).replace(" ",""),(e.net_val||0)>0&&(e.vat_val=e.net_val*n),(e.svcnet_val||0)>0&&(e.svcvat_val=e.svcnet_val*n)})),$inv.t_fds_inv()))},$inv.inRow=function(e){let t=$(this),n=e.data,i={},a=$rcol.itm.clone(["SortOrder","NameOrNumber","Type","quantityhours","UnitString","net","svcnet_val","svcvat_val","net_val","vat_val","vat","Note"]),l="N"+(65536*(1+Math.random())||0).toString(16).substr(6),r=$$.tr({id:"itm_"+l.toString(),class:"itm"});$ocms.dlgform(a,{title:t.data("dialog")||"",success:function(e){r.data($.extend({Id:l},i,e)),$inv.rrw.call(r),r.insertAfter(n),$inv.t_fds_inv()},typedvalues:!0})},$inv.eRow=function(e){let t=$(this),n=e.data,i=n.data()||{},a=["SortOrder","NameOrNumber","Type","quantityhours","UnitString","net","svcnet_val","svcvat_val","net_val","vat_val","vat","Note"];i.id||""!==(i.Type||"")||a.unshift("Type");let l=$rcol.itm.clone(a).applyValues(i);l.set("Type","hidden","type"),$inv.eRw.call(t,n,i,l)},$inv.eRw=function(e,t,n){let i=$(this);$ocms.dlgform(n,{title:i.data("dialog")||"",success:function(n){let i={};""===(t.Id||"")&&(i.Id="N"+(65536*(1+Math.random())||0).toString(16).substr(6),e.attr("id","itm_"+i.Id.toString())),i.quantity=((n.quantityhours||"").toString()+" "+(n.UnitString||"").toString()).trimEnd(),e.data($.extend({},t,n,i)),console.debug("eRw success %o",e.data()),$inv.rrw.call(e),$inv.t_fds_inv()},typedvalues:!0})},$inv.bdysort=(e,t)=>{$(t).Sortable({dragItem:!1,dragHandleClass:"ico",parentident:"tr",swapdone:(e,t,n,i)=>{$inv.t_fds_inv()}})},$inv.rrw=function(){let e=$(this),t=e.data(),n={},i=e.is(".placeholder"),a=e.is(".hidenote"),l=e=>$$.d().append(e).html(),r=[$$.dc("ibtn insb",{title:$rct.iRb}).append(gi("indent-left")).click(e,$inv.inRow)];!1===i&&(r.unshift($$.dc("ibtn edit",{title:$rct.cP}).append(gi("pencil")).click(e,$inv.eRow)),r.push($$.dc("ibtn del",{title:$rct.dR}).append(gi("trash")).click((function(t){confirm($rct.cD)&&(e.remove(),$inv.t_fds_inv())}))));let s=$$.dc("axf").append(r);!0===i?n={id:"",typ:"placeholder"}:!0===e.is(".itm.osum")?n={invrqid:t.InvRqId,id:"osum"+e.index(),typ:"osum",p:"",q:null,t:l(t.tbl.tbl),tt:null,v:null,vt:t.net_val,vs:t.svcnet_val,vat:t.vat,vv:t.vat_val,vsv:t.svcvat_val,det:!1}:(n={invrqid:t.InvRqId,id:t.Id||"",typ:t.Type||"other",p:"",q:null,t:"",tt:null,v:null,vt:t.net_val,vs:t.svcnet_val,vat:t.vat,vv:t.vat_val,vsv:t.svcvat_val,det:""!==(t.Note||"")&&!1===a},$$.dc("ibtn ico move",s,{title:$rct.mR}),n.p=t.position||t.SortOrder||"",""===n.id?n.t="":["Text","Title"].includes(n.typ)&&0===(t.net_val||0)?n.t=t.htmltext||("#"!==(t.NameOrNumber||"").substr(0,1)?l($$[0]("p").text(t.NameOrNumber)):"")+(t.Note||""):(n.tt=n.det?"":$$.s(t.Note||"").text(),n.q=t.quantity||fnum(t.quantityhours)+" "+(t.UnitString||""),n.t=t.htmltext||(n.det?l($$.s(t.NameOrNumber||""))+l($$.dc("desc").html(t.Note)):l($$.s(t.NameOrNumber||""))),n.v=t.net,n.vt=t.net_val)),""!==(t.Note||"")&&$$.dc("ibtn add",s).append(gi("object-align-left")).click((function(t){$inv.rrw.call(e.tC("hidenote"))}));let c=[$$.tdc("aux").append(s),$$.tdc("keep").text(n.p)];""===n.id?c.push($$.td(e,{colspan:4}).append(n.t)):(Array.prototype.push.apply(c,n.q?[$$.tdc("keep").text(n.q)]:[]),Array.prototype.push.apply(c,[$$.tdc("txt",{colspan:n.q?1:2,title:n.tt}).append(n.t),$$.tdc("currency").text(fnum(n.v,$rct.cst)),$$.tdc("currency inetval").text(fnum(n.vt,$rct.cst)).attr("title",$rct.svcPart+": "+fnum(n.vs,$rct.cst))])),e.empty().attr("class",i?"placeholder":"itm").aC(n.Typ).tC("hidenote",a).append(c),t.co=n},$inv.invSumUpdate=function(){let e=$(this),t=e.children("tfoot").empty(),n=bool((e.data().admin||{}).p13b||"",!1);e.nextAll(".fnote").remove();let i={ttn:0,ttb:0,ttvat:0,tscn:0,tscvat:0,vat:{},itmnet:{}},a=[],l=(e,n,i)=>$$.tdc("currency",$$.tr(t,{class:i||"tsum"}).append([$$.tdc("aux"),$$.td({colspan:4}).text(e)]),fnum(n,$rct.cst)),r=t=>$$.dc("fnote").insertAfter(e).rwText(t),s=e.children("tbody");s.each(((e,t)=>{let n=$(t),l=n.data()||{},r=[],s=[],c=null,d=0,o=n.find("tr.itm"),m=0;n.tC("empty",o.length<1),o.each(((e,t)=>{let n=$(t).data()||{};!function(e,t,n){t.tscn+=e.svcnet_val||0,t.tscvat+=e.svcvat_val||0,t.ttn+=e.net_val||0,t.ttvat+=e.vat_val||0,t.ttb+=(e.net_val||0)+(e.vat_val||0),""!==(e.vat||"")&&(t.vat[e.vat]=(t.vat[e.vat]||0)+(e.vat_val||0))}(n,i,l.Id),d+=n.net_val||0,r.push(n.co);let a=$inv.itemToContract(n);"set"===a.type&&""!==a.id?c=a.id:null!==c&&""!==(a.id||"")&&(a.setId=c),s.push(a),(void 0===n.SortOrder||null===n.SortOrder?-1:n.SortOrder)>-1&&(!1===["text","title"].includes((n.Type||"other").toLowerCase())&&m++,n.SortOrder=0,n.position=m,$inv.rrw.call(t))})),n.find("tr.isum > td.isumval").text(fnum(d,$rct.cst)),a.push({Id:l.Id,nme:l.Name,text:l.text,itm:r,items:s,netval:d})}));let c=e.find("tbody:not(.empty)").length;s.find("tr.isum").tC("hidden",c<2),l("Netto",i.ttn),!1===n?$.each(i.vat,((e,t)=>{l($rct.vat+" "+e,t,"tvat")})):i.ttb=i.ttn,l("Summe",i.ttb);let d=e.data().admin.type;"i"===d?(r($rct.note2),r($rct.note4)):"c"===d?r($rct.note2):(r(string($rct.note3,[fnum((i.tscn+i.tscvat)*(e.data().admin.tax_servicerefund||0),$rct.cst)])).aC("ntax"),r($rct.note2),r(string($rct.note1,[fnum(i.tscn+i.tscvat,$rct.cst),fnum(i.tscn,$rct.cst),fnum(i.tscvat,$rct.cst)]))),!0===n&&r($rct.note13b),e.data("sms",i),e.data("bai",a)},$inv.worknotes=function(e){let t="";return e.steps.forEach(((e,n)=>{let i;try{i=JSON.parse(e.Data||{}).fields||[]}catch(e){console.debug(e),i=[]}!0!==Array.isArray(i||"")&&(i="object"==typeof i&&!0===Array.isArray(i.field||"")?i.field:[]),i.forEach(((e,n)=>{"Ausgeführte Arbeiten"===e.name&&(t=e.result||"")}))})),t},$inv.rendersrq=function(){let e=$(this).empty(),t=e.is(".onesum"),n=e.data(),i=$$.tr(e,{id:"srq"+n.Id}).aC("title nosort"),a=($rcol.itm.lbl(),$$.dc("axf").appendTo($$.tdc("aux",i)));$$.dc("ibtn osum",a,{title:$rct.combP}).append(gi("euro")).click((function(t){e.tC("onesum"),$inv.rendersrq.call(e),$inv.t_fds_inv()})),$$.dc("ibtn setvat",a,{title:$rct.sV}).append(gi("gbp")).click(i,$inv.setVat),$$.dc("ibtn insb",a,{title:$rct.iRb}).append(gi("indent-left")).click(i,$inv.inRow);let l,r=$$.sc("text",n.text),s=($$.td(i,{colspan:t?4:5}).append(r),["net_val","vat_val","svcnet_val","svcvat_val","net"]);if($$.dc("ibtn edit",a).data("dialog",$rcol.req.lbl().Name).append(gi("pencil")).click({t:r,change:e=>{n.text=e,$inv.t_fds_inv()}},$inv.eHtml),t&&($$.tdc("currency isumval",i),l={Id:n.Id.toString()+"_osum",net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0},l.tbl=$$.tblset({class:"stbl"})),$.each(n.items||[],((n,i)=>{let a,r={Id:i.Id,net_val:i.net_val||0,vat_val:i.vat_val||0,svcnet_val:0,svcvat_val:0,net:i.net||0,Note:i.Note||""};if("service"===i.Type.toLowerCase())r.svcnet_val=i.net_val||0,r.svcvat_val=i.vat_val||0;t?(a=$$.tr(l.tbl.bdy,{id:"itm"+i.Id,class:"sitm"}).aC(i.Type),"Text"===i.Type||"Title"===i.Type?$$.td(a,{colspan:2}).html(i.htmltext||i.Note):($$.tdc("keep",a).text(i.quantity||((i.quantityhours||0)>0?fnum(i.quantityhours)+(i.UnitString||"").eine(" ",""):"")),i.htmltext?$$.tdc("txt",a).html(i.htmltext):$$.tdc("txt",a).text(i.NameOrNumber).attr("title",i.Note)),$.each(s,((e,t)=>{l[t]+=r[t]})),a.data(r)):($.extend(r,i),a=$$.tr(e,{id:"itm"+i.Id,class:"itm"}),a.data(r),$inv.rrw.call(a))})),t){let t=$$.tr(e,{id:"itmsq"+n.Id,class:"itm osum"}).data(l);$inv.rrw.call(t)}else{let t=$$.tr(e).aC("isum nosort");$$.tdc("aux",t),$$.td(t,{colspan:4}).text($rct.iSum),$$.tdc("currency isumval",t)}},$inv.t_fds_inv=()=>{$("div.invoice_layout table.invi").trigger("fds.inv")},$inv.sedit=()=>{$inv.sprev(!0)},$inv.jdisp=function(e){e.stopPropagation(),e.data.id&&$inv.disp(e.data.id,e.data.typ||"")},$inv.disp=(e,t)=>{let n="";switch(t){case"inv":n="inv/rdoc";break;case"rem":n="rem/rdoc"}""!==n&&$ocms.postXT({url:$ocms.url(n),data:{id:e||"",typ:"img"},success:e=>{let t=$$.dc("imagecollection pdfpreview"),n=Math.round(.88*vh());e.id;$.each(e.img||[],(function(e,i){$$.dc("pdfp",t).append($$.img(i).css("max-height",(n-rpx(6)).toString()+"px"))})),$ocms.dlg(t,{size:[n,Math.round(.88*vw())],zindex_min:50,form:!1,exclusive:!1})}})},$inv.jdbn=function(e){$ocms.postXT({url:$ocms.url("inv/rdocn"),data:{name:e.data.id||"",typ:"img"},success:e=>{let t=$$.dc("imagecollection pdfpreview"),n=Math.round(.88*vh());e.id;$.each(e.img||[],(function(e,i){$$.dc("pdfp",t).append($$.img(i).css("max-height",(n-rpx(6)).toString()+"px"))})),$ocms.dlg(t,{size:[n,Math.round(.88*vw())],zindex:50,form:!1})}})},$inv.sp13b=()=>{var e=$("div.invoice_layout").find("table.invi"),t=e.data();t.admin.p13b=!0,!1===(t.inv.InvoiceOptions||"").split(",").includes("§13b")&&(t.inv.InvoiceOptions+=",§13b"),e.trigger("fds.inv")},$inv.itemToContract=function(e){let t=((e=e||{}).Type||"").toString().toLowerCase(),n={id:(e.Id||"").toString(),type:t,title:"",desc:"",qty:"",price_net:"",total_net:e.net_val||0,vat:e.vat||""};var i;return e.co&&"osum"===e.co.typ?(n.desc=e.co.t||"",n.total_net=e.net_val||0):["text","title"].includes(t)&&0===(e.net_val||0)?(n.desc=e.htmltext||("#"!==(e.NameOrNumber||"").substr(0,1)?(i=$$[0]("p").text(e.NameOrNumber||""),$$.d().append(i).html()):"")+(e.Note||""),n.total_net=""):(e.htmltext?n.desc=e.htmltext:(n.title=e.NameOrNumber||"",n.desc=e.Note||""),n.qty=e.quantity||(0!==(e.quantityhours||0)?fnum(e.quantityhours)+(e.UnitString?" "+e.UnitString:""):""),n.price_net=e.net||0,n.total_net=e.net_val||0),n},$inv.ssetmode=()=>{let e=$("div.invoice_layout").find("table.invi").data();e.admin=e.admin||{};let t,n=e.admin.setmode||"setprice",i=e=>$$.dc("btn",$ict.setmo[e]).tC("selected",n===e).click((()=>{t.c.trigger("modal_close"),$inv.setSetmode(e)})),a=$$.dc("choicefrm").append([i("setprice"),i("itemprices"),i("setonly")]);t=$ocms.dlg(a,{width:800})},$inv.setSetmode=e=>{let t=$("div.invoice_layout").find("table.invi").data();t.admin=t.admin||{},t.admin.setmode=e,t.inv=t.inv||{};let n=(t.inv.InvoiceOptions||"").split(",").filter((e=>""!==e&&0!==e.indexOf("setmode:")));e&&"setprice"!==e&&n.push("setmode:"+e),t.inv.InvoiceOptions=n.join(",")},$inv.sctp=()=>{let e=$invcol.ctp;$ocms.dlgform(e,{title:$ict.ctp,success:function(e){var t=$("div.invoice_layout"),n=t.find("table.invi").data();let i={};void 0!==n.new&&"{"===(n.new.CustomValues||"").substr(0,1)&&(i=JSON.parse(n.inv.CustomValues)),i.contactName=e.name,i.contactEmail=e.email,n.new.CustomValues=JSON.stringify(i),t.find(".ctpfrm").text(ne(e.name,e.email))},typedvalues:!0})},$inv.invcPayload=function(e){let t=(e=e||{}).sms||{},n=$.extend({},e.new),i=$.extend({},e.admin);return n.total_net=t.ttn||0,n.total_gross=t.ttb||0,n.title=null!=n.invoicetitle?n.invoicetitle:n.title||"",n.provisionlocation=null!=n.loc?n.loc:n.provisionlocation||"",n.paymentterm=null!=i.paymentterms?i.paymentterms:n.paymentterm||"",i.customerid=null!=i.customerid?i.customerid:i.CustomerId,{admin:i,req:e.bai,sms:e.sms,new:n}},$inv.ssave=()=>{var e=$("div.invoice_layout"),t=e.find("table.invi").data();$inv.t_fds_inv(),e.aC("freeze"),$ocms.postXT({url:$ocms.url("req/save"),data:{invc:JSON.stringify($inv.invcPayload(t)),id:t.invid||""},success:e=>{$inv.cntInv({id:e.id})},error:()=>{alert($ict.eis)},complete:()=>{e.rC("freeze")}})},$inv.sprev=e=>{var t=$("div.invoice_layout"),n=t.find("table.invi").data();e=bool(e,!1),$inv.t_fds_inv(),t.aC("freeze"),!1!==$fis.ValidateEmail(n.new.invoiceemail||"")||!1!==bool(confirm($ict.ivE+$ict.ivEc),!1)?$ocms.postXT({url:$ocms.url("req/"+(!0===e?"sedit":"sprep")),data:{invc:JSON.stringify($inv.invcPayload(n)),id:n.invid||""},success:e=>{let n=$$.dc("imagecollection pdfpreview"),i=Math.round(.88*vh()),a=e.id,l=e.total;l>10&&$$.dc("note warn",n).text($ict.tpe),$.each(e.img||[],(function(e,t){$$.dc("pdfp",n).append($$.img(t).css("max-height",(i-rpx(6)).toString()+"px"))}));for(let t=(e.img||[]).length+1;t<=l;t++)$$.dc("pdfp ph",n).append($$.dc("note",$ict.pna));$ocms.dlg(n,{size:[i,Math.round(.88*vw())],zindex:50,form:!1,button:$rct.crI,confirm:function(e){let n=$(this);t.aC("freeze"),$ocms.postXT({url:$ocms.url("req/sconf"),data:{id:a},success:e=>{n.trigger("modal_close"),!0===e.hasFile&&window.open($ocms.url("req/idoc")+"?id="+a,"_blank"),$ocms.init("req"),$inv.rReload()},error:()=>{alert($t.f1),n.trigger("modal_close")},complete:()=>{t.rC("freeze")}})},cancel:function(e){$(this);confirm($ict.cdI)&&$ocms.postXT({url:$ocms.url("req/sdel"),data:{id:a}}),$inv.rReload()}})},error:()=>{alert($ict.eis)},complete:()=>{t.rC("freeze")}}):t.rC("freeze")},$inv.rReload=()=>{try{let e=$("#listframe ul.rql:first").data();$inv.cInv2({id:e.search})}catch(e){}},$inv.quantChange=function(e){let t=$(this).closest("form"),n={},i=e=>parseFloat(e.toString().replace("%","").replace(",",".")),a=e=>e.toFixed(2);t.find(":input").each(((e,t)=>{n[$(t).attr("name")]=$(t)}));let l=parseInt(n.quantityhours.val()||"0"),r=i(n.net.val()||"0"),s=.01*i(n.vat.val());l>0&&r>0&&(n.net_val.val(a(l*r)),n.vat_val.val(a(l*r*s)),["Service"].includes(n.Type.val())&&(n.svcnet_val.val(a(l*r)),n.svcvat_val.val(a(l*r*s))))},$inv.storno=function(e,t){let n,i=$$.dc("choicefrm").append([$$.dc("btn","Storno ohne Details").click({id:e,mode:"simple"},(e=>{n.c.trigger("modal_close"),$inv.cSt(e.data)})),$$.dc("btn","Storno mit neuer Rechnung").click({id:e},(e=>{n.c.trigger("modal_close"),$inv.ccStInv(e)})),$$.dc("btn","Storno mit best. Rechnung").tC("inactive",!1===bool(t,!1)).click({id:e,mode:"copy"},(e=>{!0===bool(t,!1)&&(n.c.trigger("modal_close"),$inv.cSt(e.data))}))]);n=$ocms.dlg(i,{width:1e3})},$inv.credit=function(e,t){let n,i=$$.dc("choicefrm").append([$$.dc("btn","Gutschrift").click({id:e,mode:"credit"},(e=>{n.c.trigger("modal_close"),$inv.cSt(e.data)}))]);n=$ocms.dlg(i,{width:1e3})},$inv.setPyd=function(e){confirm($ict.cpyd)&&$ocms.postXT({url:$ocms.url("inv/setpyd"),timeout:60,data:{id:e},success:e=>{alert($ict.relm)},error:()=>{alert($t.f1)}})},$inv.setUpd=function(e){confirm($ict.cupd)&&$ocms.postXT({url:$ocms.url("inv/setupd"),timeout:60,data:{id:e},success:e=>{alert($ict.relm)},error:()=>{alert($t.f1)}})},$inv.resendRem=function(e){e.stopPropagation(),e.data.id&&confirm(string($ict.remresc,[e.data.name]))&&$ocms.postXT({url:$ocms.url("rem/resend"),timeout:60,data:{id:e.data.id},success:t=>{alert(string($ict.remresr,[e.data.name]))},error:()=>{alert($t.f1)}})},$inv.dspRem=function(e){let t=$$.dc("rfrm").ldng(1),n=$ocms.dlg(t,{width:1e3});n.ft.rwText($rct.rq2),$ocms.postXT({url:$ocms.url("inv/getrem"),timeout:60,data:{id:e,drafts:!1},success:e=>{n.ft.empty();let i=$$.tblset({class:"invtbl"},t.empty()),a=$invcol.rem2,l=$$.tr(i.hd);$$.th(l);$.each(a.fields||[],((e,t)=>{$$.th(l).text(t.label)}));let r=!1;$.each(e,((e,t)=>{r=!r;let n=$$.tr(i.bdy).tC("alt",r),l=$$.td(n);n.click((function(){n.tC("selected").siblings().rC("selected")})),!0===bool(t.hasFile,!1)&&($$.dc("idl ilbtn",l,{title:$ict.dl+"\n"+t.DocumentName}).append(gi("save-file","ico")).click({id:t.Id},$inv.downloadrem),$$.dc("idl ilbtn",l,{title:$ict.remdsp+"\n"+t.DocumentName}).append(gi("eye-open","ico")).click({id:t.Id,typ:"rem"},$inv.jdisp),$$.dc("idl ilbtn",l,{title:$ict.remres+"\n"+t.DocumentName}).append(gi("refresh","ico")).click({id:t.Id,typ:"rem",name:t.DocumentName},$inv.resendRem)),$.each(a.fields||[],((e,i)=>{let a=$$.td(n).aC(i.dtype),l=t[i.name];if("function"==typeof i.dfnc)i.dfnc.call(a,l,t);else switch(i.type||""){case"date":a.text(fdt(t[i.name],"dd.MM.yy"));break;case"datetime":a.text(fdt(t[i.name]));break;case"html":a.append($$.dc("ctw").html(l)),a.append($$.dc("ttip").html(l));break;default:a.text(t[i.name])}if("InvoiceId"===(i.name||""))a.aC("keep");switch(typeof i.title){case"function":i.title.call(a,t);break;case"string":a.attr("title",cs.title)}}))}))},error:()=>{t.empty(),n.ft.rwText($t.f1)},complete:()=>{t.ldng(0)}})},$inv.ccRem=function(e,t){$(this);$ocms.postXT({url:$ocms.url("rem/lrem"),timeout:60,data:{id:e},success:n=>{let i=$invcol.rid.clone();i.applyValues(n.ov);let a=$$.dc("ac"),l=$$.tblset({class:"fullgrid fullwidth"},a);if((n.lst||[]).length>0){$$.d({style:"margin: 1.5rem 0 1rem 0;font-size: 110%;text-decoration: underline;"}).prependTo(a).text($ict.rovlh);let e=$$.tr(l.hd);$ict.rovl.forEach(((t,n)=>$$.th(e,t))),$.each(n.lst,((e,t)=>{$$.tr(l.bdy).append([$$.tdc("keep",t.subject),$$.tdc("currency",fnum(t.amount,$rct.cst)),$$.tdc("currency",fnum(t.amount_payed,$rct.cst)),$$.tdc("keep",fdt(t.DateFinalized,"dd.MM.yy"))])}))}else $$.td($$.tr(l.bdy),$ict.nd);$ocms.dlgform(i,{addcontent:a,title:string($ict.remdt,[t||"?"]),success:function(t){$inv.ccRem_s2(e,t)},typedvalues:!0})}})},$inv.rRemRw=function(e){let t=$(this),n=e.rm||{};t.empty().data({invoiceid:n.invoiceid,invoicedate:n.invoicedate,amount:n.amount,amount_payed:n.amount_payed});let i=$$.dc("axf").append($$.dc("ibtn edit",{title:$rct.cP}).append(gi("pencil")).click(t,$inv.eRowR));t.append([$$.tdc("aux").append(i),$$.tdc("keep",n.invoiceid),$$.tdc("keep",fdt(n.invoicedate,"dd.MM.yy")),$$.tdc("currency",fnum(n.amount,$rct.cst)),$$.tdc("currency",fnum(n.amount_payed,$rct.cst)),$$.tdc("currency",fnum(n.amount-n.amount_payed,$rct.cst))])},$inv.eRowR=function(e){let t=$(this),n=e.data,i=n.data()||{},a=$invcol.rem.clone().applyValues(i);$ocms.dlgform(a,{title:t.data("dialog")||"",success:function(e){let i=t.closest("table"),a=i.data();$.extend(a.rm,e),i.data(a),$inv.rRemRw.call(n,a)},typedvalues:!0})},$inv.ccRem_s2=function(e,t){$fis.lf(!1).rC("fix").aC("hd");let n=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.rprev));$inv.eM(!1,!0);$$.dc("rfrm").ldng(1);$ocms.postXT({url:$ocms.url("rem/get"),timeout:60,data:$.extend({id:e},t),success:e=>{let t=e.rm||{},i=$$.dc("srq",n);$ict.remt[t.type].forEach((e=>$$[0]("p").rwText(e).appendTo(i)));let a=$$.tblset({class:"invi"},i);a.ft=$$[0]("tfoot"),a.tbl.data($.extend({invid:t.invid,new:{}},e));let l=$$.tr(a.hd).aC("shd").append([$$.th().aC("aux")]);$ict.remHR.forEach((e=>$$.th(l,e))),$inv.rRemRw.call($$.tr(a.bdy),a.tbl.data()),a.ft.appendTo(a.tbl),$ict.remt2[t.type].forEach((e=>$$[0]("p").rwText(e).appendTo(i)));let r=(e,t,i,l,r)=>{let s=$$.dc("inpfrm",n).aC(e).append("string"==typeof l?$$.dc("ahd",l):l>0?$$.dc("ahd",$rct.frm[i]):null),c=$$.dc("content",s).rwText(t);$$.dc("axf",s).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:c,nme:i,change:e=>{a.tbl.data("new")[i]=e}},r),$inv.eHtml)),a.tbl.data("new")[i]=t};r("tfrm",t.subject,"subject",0,null),r("adrfrm",t.invoiceaddress,"invoiceaddress",0,null),r("emailfrm",t.invoiceemail,"invoiceemail",0,null),$$.dc("sndfrm",n).append($$.dc("content").text(t.sender)),a.tbl.children("tbody").each($inv.bdysort),a.tbl.trigger("fds.inv")},complete:()=>{}})},$inv.rprev=()=>{var e=$("div.invoice_layout"),t=e.find("table.invi"),n=t.data();$.extend(n.new,t.find("tbody > tr:first").data()),e.aC("freeze"),!1!==$fis.ValidateEmail(n.new.invoiceemail||"")||!1!==bool(confirm($ict.ivE+$ict.ivEc),!1)?$ocms.postXT({url:$ocms.url("rem/prep"),data:{remc:JSON.stringify({rem:n.rm,new:n.new}),id:n.invid||""},success:t=>{e.rC("freeze");let n=$$.dc("imagecollection pdfpreview"),i=Math.round(.88*vh()),a=t.id;$.each(t.img||[],(function(e,t){$$.dc("pdfp",n).append($$.img(t).css("max-height",(i-rpx(6)).toString()+"px"))})),$ocms.dlg(n,{size:[i,Math.round(.88*vw())],zindex:50,form:!1,button:$ict.remd,confirm:function(e){let t=$(this);$ocms.postXT({url:$ocms.url("rem/conf"),data:{id:a},success:()=>{t.trigger("modal_close"),window.open($ocms.url("rem/idoc")+"?id="+a,"_blank"),$ocms.init("req"),$inv.rReload()},error:()=>{alert($t.f1),t.trigger("modal_close")}})},cancel:function(e){$(this);confirm($ict.cdI)&&$ocms.postXT({url:$ocms.url("rem/del"),data:{id:a}}),$inv.rReload()}})}}):e.rC("freeze")},$inv.sis=e=>{confirm($ict.sisc)&&$ocms.postXT({url:$ocms.url("inv/sis"),data:{id:e||""},success:e=>{}})},$inv.srs=e=>{confirm($ict.srsc)&&$ocms.postXT({url:$ocms.url("rem/srs"),data:{id:e||""},success:e=>{}})},$inv.mfrrel=e=>{$("#contentframe").ldng(),$ocms.postXT({url:$ocms.url("inv/mfrrel"),data:{id:e||""},success:e=>{$inv.rerenderinv()},complete:()=>{$("#contentframe").ldng(0)}})}; \ No newline at end of file +let $rct={mdl:"Aufträge",or:"offene Aufträge",orr:"offene Aufträge (4 W)",rn:"Auftragsnummer",iov:{all:"Auftragsübersicht (alle)","":"Auftragsübersicht"},wk:"Woche",nd:"Keine Daten gefunden.",h:"Uhr",rq1:"Auftragsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.",rq2:"Auftragsdaten werden geladen",rq1f:"Die Auftragsdaten von MFR konnten nicht oder nicht schnell genug abgerufen werde.\nMöchten Sie mit den bestehenden Daten trotzdem weitermachen?",note1:"Im Bruttobetrag sind {0} Lohnkosten enthalten (netto {1}). Die darin enthaltene Umsatzsteuer beträgt {2}.",note2:"Bitte beachten Sie, nach §14 Abs. 1 Umsatzsteuergesetz ist diese Rechnung ein Zahlungsbeleg oder eine andere beweiskräftige Unterlage für 2 Jahre nach Ablauf des Kalenderjahres der Ausstellung dieser Rechnung aufzubewahren, soweit nicht aufgrund anderer gesetzlicher Regelungen andere ggf.längere Aufbewahrungsfristen gelten.",note3:"Privathaushalten erstattet das Finanzamt bis zu {0} des Arbeitslohns mit der nächsten Steuererklärung.",note4:"Für bereits erbrachte Arbeiten, Dienstleistungen, Materiallieferungen und getätigte Bestellvorgänge zum oben genannten Bauvorhaben, die sich aus dem mit Ihnen geschlossenen Vertrag ergeben, stellen wir Ihnen vertragsgemäß unsere Akontozahlung in Rechnung. Eine Endabrechnung erhalten Sie als Schlussrechnung nach Abschluss des gesamten Bauvorhabens. Das Ausführungsdatum entnehmen Sie bitte dem Schlusstext dieser Rechnung. Wir danken Ihnen herzlich für das entgegengebrachte Vertrauen und bitten Sie um kurzfristigen Ausgleich der Akontorechnung.",note13b:"Gem. §13b Umsatzsteuergesetz unterliegen Sie der Steuerschuldnerschaft des Leistungsempfängers zur Umsatzsteuer aus dieser Rechnung mit einem Steuersatz von 19%.",crI:"Rechnung erstellen",crII:"Abschlagsrechnung erstellen",dII:"Für eine Abschlagsrechnung darf nur ein Auftrag gewählt werden.",dnS:"Für eine Rechnung muss mindestens ein Auftrag gewählt werden.",inv:"Rechnung",invs:"Rechnungen",req:"Auftrag",provP:"Leistungszeitraum",provD:"Leistungsdatum",cP:"Position ändern",iRb:"Zeile darunter einfügen",dR:"Zeile löschen",sV:"USt festlegen",cD:"Löschen?",mR:"Zeile verschieben",svcPart:"Service-Anteil",vat:"Umsatzsteuer",combP:"Positionen zusammenfassen",toSetP:"Auf Setpreis umstellen (Positionen ohne Preis)",toSetPc:"Der Set-Preis wird auf die Summe seiner Positionen gesetzt und die Positionen selbst verlieren ihren Preis. Das ist nicht rückgängig zu machen (der Betrag kann danach nur noch manuell geändert werden). Fortfahren?",toSetMc:"Jeder Leistungsauftrag wird zu einer Set-Position mit der Summe als Preis zusammengefasst. Das ist nicht rückgängig zu machen (die Set-Preise können danach nur noch manuell geändert werden). Fortfahren?",iSum:"Zwischensumme",dtRel:"Freigegeben am: ",dtCr:"Erstellt am: ",rqV:"USt des Auftrags?",cthd:"wirklich aus-/einblenden ?",cst:{style:"currency",currency:"EUR"},sts:{IsWorkDone:"Arbeiten erledigt",Closed:"Auftrag geschlossen",SubcontractorPendingConfirmation:"Warten auf Bestätigung (Unterauftrag)",Scheduled:"Geplant",OfferIsRejected:"Angebot abgelehnt",OfferIsSend:"Offen (Angebot versandt)",CollaborationWaitingConfirmation:"Warten auf Bestätigung (Zusammenarbeit)",Released:"Freigegeben",OfferIsConfirmed:"Bestätigt",InProgress:"In Bearbeitung",ReadyForScheduling:"Zur Planung",Created:"Erstellt",Rejected:"Abgebrochen",Invoiced:"Rechnung gestellt","-":"-"},invHR:["Pos.","Menge","Artikelbezeichnung","VK","Summe"],frm:{invoiceaddress:"Adresse",loc:"Leistungsort / Lieferadresse",invoiceemail:"Email"}},$rcol={req:new fields_definition("Auftrag","Aufträge",[{name:"tags",label:"",type:"string",dfnc:function(e,t){""!==(e||"")&&($(this).aC("tags"),e.split(",").forEach((e=>{""!==e&&$(this).append($$.sc("tag "+e.replace(" ","_").replace("/","_").toLowerCase(),e))})))}},{name:"DateOfCreation",label:"Datum",type:"date",title:function(e){$(this).attr("title",$rct.dtCr+fdt(e.DateOfCreation).ne("-")+" \n"+$rct.dtRel+fdt(e.DateReleased).ne("-"))}},{name:"CustomerName",label:"Kunde (Firma)",type:"string"},{name:"Name",label:"Auftragsname",type:"string"},{name:"ExternalId",label:"Auftragsnummer",type:"string"},{name:"ParentExtenalId",label:"PAuftrag",type:"string"},{name:"InvoiceId",label:"RNummer",type:"string",dfnc:function(e,t){$(this).rwText(e," ").find("span").each((function(){$(this).aC("cla").click({id:$(this).text()},$inv.jdbn)}))}},{name:"State",label:"Status",type:"string"},{name:"WorkDoneAt",label:"Erledigt am",type:"date"},{name:"Description",label:"Beschreibung",type:"html"}]),itm:new fields_definition("Auftragsposition","Auftragspositionen",[{name:"NameOrNumber",label:"Bezeichnung",type:"string"},{name:"Type",label:"Typ",type:"select",required:!0,value:"Text",url:[{value:"Text",label:"Text"},{value:"Equipment",label:"Ausrüstung"},{value:"Material",label:"Material"},{value:"Service",label:"Arbeitsleistung"}],change:function(e){$req.quantChange.call(this,e)}},{name:"quantityhours",label:"Anzahl / Menge",type:"number",precision:"0.01",value:1,change:function(e){$inv.quantChange.call(this,e)}},{name:"UnitString",label:"Einheit",type:"select",url:["LFDM","Stck","Std.","QM","AW","Pauschal"],change:function(e){$inv.quantChange.call(this,e)}},{name:"net",label:"EinzelPreis netto",type:"number",precision:"0.01",value:0,change:function(e){$inv.quantChange.call(this,e)}},{name:"net_val",label:"GesamtPreis netto",type:"number",precision:"0.01",value:0},{name:"vat_val",label:"GesamtPreis USt",type:"number",precision:"0.01",value:0},{name:"svcnet_val",label:"Arbeitslohn netto",type:"number",precision:"0.01",value:0},{name:"svcvat_val",label:"Arbeitslohn USt",type:"number",precision:"0.01",value:0},{name:"net_pos",label:"Netto",type:"string"},{name:"bo_pos",label:"Brutto",type:"string"},{name:"vat",label:"USt",type:"string",value:"19,0%",change:function(e){$inv.quantChange.call(this,e)}},{name:"Note",label:"Details",type:"html",tinymce:!0}])},$ict={mdl:"Rechnungen",iov:{all:"Rechnungen (alle)","":"Rechnungen (nur fertige)","#d":"Rechnungen (nur Entwürfe)","#u":"Rechnungen (nur unbezahlt)","#r":"Rechnungen (nur angemahnt)","#a":"Rechnungen (nur Akonto)","#c":"Rechnungen (nur Storno)","#ru":"Rechnungen (nur angemahnt + unbez.)"},uba:", gesamter Zeitraum)",req:"Auftrag",inv:"Rechnung",rem:"Mahnung",in:"Rechnungsnummer",cc:"Kunde",wk:"Woche",nd:"Keine Daten gefunden.",dl:"Herunterladen",ed:"Bearbeiten",ced:"Bearbeitung fortsetzen",sItm:"Einzelheiten anzeigen",sPay:"Zahlungen anzeigen",cdI:"Entwurf der Rechnung löschen?",rel:"Neu Laden",relm:"Bitte laden Sie Liste manuell neu, um die Änderungen zu sehen.",dsp:"Rechnung anzeigen",storno:"Storno-Rechnung erstellen",credit:"Gutschrift erstellen",remd:"Mahnung erstellen",remdt:"Mahnung erstellen zur Rechnung {0}",remlst:"Mahnungen anzeigen",remdsp:"Mahnung anzeigen",remres:"Mahnung erneut senden",remresc:"Mahnung {0} wirklich erneut senden?",remresr:"Mahnung {0} wurde erfolgreich versandt.",setpyd:"Bezahlt markieren",cpyd:"Rechnung wirklich als bezahlt markieren?",setupd:"Bezahlt-Markierung aufheben",cupd:"Bezahlt-Markierung wirklich aufheben?",ivE:"Die Email-Adresse ist vermutlich nicht gültig.",ivEc:"\nMöchten Sie fortfahren?",pna:"Diese Seite ist in der Vorschau nicht verfügbar",tpe:"Die Anzahl von {0} Seiten wird aktuell nicht unterstützt",eis:"Der Rechnungsentwurf konnte nicht gespeichert werden.",iss:"Zwischenstand speichern.",p13b:"USt -> §13b",setm:"Set-Preisanzeige",setmo:{setprice:"Set mit Preis – Positionen ohne Preis",setonly:"Nur Set mit Preis – Positionen ausgeblendet"},ctp:"Ansprechpartner festlegen",mfr:"Von MFR neu abrufen",rq1:"Auftragsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.",rq2:"Auftragsdaten werden geladen",iq1:"Rechnungsdaten werden von MFR abgerufen.\nDer Vorgang kann bis zu 90Sek dauern.",iq2:"Rechnungsdaten werden geladen",sis:"Rechnung als versandt markieren",srs:"Mahnung als versandt markieren",sisc:"Rechnung wirklich als versandt markieren?",srsc:"Mahnung wirklich als versandt markieren?",iSt:{dft:"Entwurf",uns:"nicht versandt",pyd:"bezahlt",cc:"storniert",op:"offen",due:"fällig",ovd:"überfällig",rem:"angemahnt"},rSt:["","Überfällig","2. Mahnung","3. Stufe"],pSt:{a:"Vollst.",p:"Teilz."},ivT:{i:"AbschlagsR.",f:"SchlussR",r:"Rechnung",c:"StornoR."},rovlh:"Übersicht der bisherigen Mahnungen",rovl:["Betreff","Betrag","Betrag gezahlt","fertiggestellt am"],remHR:["Rechnung","vom","Rechnungsbetrag","bereits bezahlt","noch offen"],translatePaymentTerm:e=>(e||"").replace("wd"," Werktagen").replace("d"," Tagen").replace("wk"," Wochen")||"10 Tagen",payi:{account:"Konto",name:"Zahler",text:"Verw.Zweck",InvoiceID:"Rechnung",amount:"Betrag",date:"Datum",manual:"Typ"}},$invcol={datev:new fields_definition("Rechnung","Rechnungen",[{name:"Umsatz (ohne Soll/Haben-Kz)",label:"Umsatz (ohne Soll/Haben-Kz)",type:"string"},{name:"vf",label:"vf",type:"string"},{name:"Soll/Haben-Kennzeichen",label:"Soll/Haben-Kennzeichen",type:"string"},{name:"Konto",label:"Konto",type:"string"},{name:"Gegenkonto",label:"Gegenkonto",type:"string"},{name:"BU-Schlüssel",label:"BU-Schlüssel",type:"string"},{name:"Belegdatum",label:"Belegdatum",type:"string"},{name:"Belegfeld 1",label:"Belegfeld 1",type:"string"},{name:"Belegfeld 2",label:"Belegfeld 2",type:"string"},{name:"Buchungstext",label:"Buchungstext",type:"string"}]),inv:new fields_definition("Rechnung","Rechnungen",[{name:"invstatus",label:"Status",type:"select",url:$ict.iSt},{name:"balance",label:"Umsatz",type:"string",dtype:"currency"},{name:"CustomerName",label:"Kunde",type:"string"},{name:"InvoiceId",label:"RNummer",type:"string"},{name:"InvoiceType",label:"Typ",type:"select",url:$ict.ivT},{name:"request",label:"Auftrag",type:"string",dtype:"num"},{name:"vat",label:"MwSt",type:"string",dtype:"num"},{name:"deb_cred",label:"Soll/Haben",type:"string"},{name:"customer",label:"Konto",type:"string",dtype:"num"},{name:"contra_account",label:"Gegenkonto",type:"string",dtype:"num"},{name:"Belegdatum",label:"Belegdatum",type:"date"},{name:"reminderstatus",label:"MahnStatus",type:"select",url:$ict.rSt},{name:"reminder",label:"# Mahnungen",type:"integer"},{name:"Buchungstext",label:"Buchungstext",type:"string"},{name:"Payment",label:"Zahlung",type:"string"}]),rem:new fields_definition("Zahlungserinnerung","Zahlungserinnerung",[{name:"amount",label:"Rechnungsbetrag",type:"number",precision:"0.01",value:1},{name:"amount_payed",label:"bereits bezahlt",type:"number",precision:"0.01",value:1}]),rem2:new fields_definition("Zahlungserinnerung","Zahlungserinnerung",[{name:"DocumentName",label:"Name",type:"string"},{name:"subject",label:"Betreff",type:"string"},{name:"DateSent",label:"Versanddatum",type:"date"},{name:"status",label:"Status",type:"string"},{name:"amount_open",label:"offener Betrag",type:"number",precision:"0.01"},{name:"InvoiceId",label:"RNummer",type:"string"}]),rid:new fields_definition("Zahlungserinnerung","Zahlungserinnerung",[{name:"type",label:"Typ",type:"select",url:[["f","einfache Zahlungserinnerung"],["m","Mahnung"],["l","letzte Mahnung"]],required:!0},{name:"level",label:"Stufe",type:"select",url:[["1","Stufe 1"],["2","Stufe 2"],["3","Stufe 3"],["4","Stufe 4"],["5","Stufe 5"],["6","Stufe 6"]],required:!0}]),ctp:new fields_definition("Ansprechpartner","Ansprechpartner",[{name:"name",label:"Name",type:"string"},{name:"email",label:"Email",type:"string"}])},gi=(e,t)=>$$.sc("glyphicon glyphicon-"+e).aC(t),$inv={init2:function(e,t){e=e||"inv",t=t||{},$ocms.getScript([],(function(){$inv.init3(e,t)}))},init3:async function(e,t){$fis.cf(!0);let n=$fis.lf(!0);$("#topbar").ocmsmenu([]),$("#activemodule").text($ict.mdl);let i=[(async()=>{await $fis.getAuth("fds_inv")>0&&($inv.prepLst(""),n.aC("fix"))})(),new Promise(((e,t)=>{$fis.prepAuth(["fds_reminder"])}))];await Promise.all(i)},prepLst:function(e){let t=new Date,n=$fis.lf(!0).ldng(1),i=new Date("2021-01-01");$fis.frm_list().IN((function(){}));let a=[];$.each($ict.iov,((e,t)=>{a.push({lbl:t,fnc:()=>{$inv.prepLst(e),n.aC("fix")}})})),$fis.lfm().ocmsmenu([{lbl:"Filter",itm:a}]);$$.i({placeholder:$ict.in}).appendTo($$.dc("mth ivn",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>3&&(t.parent().aC("selected"),$inv.renderinv("i:"+n,"s","all"),t.val(""))})),$$.i({placeholder:$ict.cc}).appendTo($$.dc("mth ivc",n)).enterKey((function(e){let t=$(this),n=t.val()||"";e.stopPropagation(),t.parent().siblings().rC("selected"),n.length>=3&&(t.parent().aC("selected"),$inv.renderinv("c:"+n,"s","all"),t.val(""))}));"#"===e.substr(0,1)&&$$.dc("mth extra",n).text($ict.iov[e].replace(")",$ict.uba)).click((function(t){let n=$(this);if(t.stopPropagation(),n.siblings().rC("selected"),!0===n.is(".selected")){n.toggleClass("selected");let t=fdt(new Date,"yy-MM-dd");$inv.renderinv(t,"a",e)}n.aC("selected")})),n.append("
");let r=$$.dc("mthl",n),l=t.getFullYear(),d=t.getMonth()+1;for(let t=i.getFullYear();t<=l;t++){let n=$$.dc("yr").prependTo(r).text($ict.iov[e]+" - "+t.toString()).toggleClass("selected",t===l);n.click({yr:t},(function(e){e.stopPropagation(),n.siblings().rC("selected"),n.aC("selected")}));let a=$$.dc("mfrm",n);for(let n=0;n<(t!==l?12:d);n++){i=new Date(t,n,1);let r=$$.dc("mth").prependTo(a).text($ict.iov[e]+" - "+fdt(i,"MMM yyyy"));if(r.click({yr:t,mt:n},(function(t){if(t.stopPropagation(),r.siblings().rC("selected"),!0===r.is(".selected")){r.toggleClass("selected");let n=fdt(new Date(t.data.yr,t.data.mt,1),"yy-MM-dd");$inv.renderinv(n,"m",e)}r.aC("selected")})),""===e){$$.dc("mthdl",r).append(gi("compressed","ico")).click({yr:t,mt:n},(function(e){e.stopPropagation();let t=fdt(new Date(e.data.yr,e.data.mt,1),"yy-MM-dd");$inv.downloadzip(t,"m")}))}let l=getMonday(i),d=new Date(i);d.setMonth(d.getMonth()+1),d.setDate(0),d=getMonday(d);let s=$$.dc("wfrm",r);for(;l<=d;){let t=$$.dc("wk",s).text(($ict.wk||"W")+" "+fdt(l,"dd.MM.yy"));t.click({rd:new Date(l)},(function(n){n.stopPropagation();let i=fdt(n.data.rd,"yy-MM-dd");$inv.renderinv(i,"w",e),r.siblings().rC("selected").find(".wk").rC("selected"),r.aC("selected").find(".wk").rC("selected"),t.aC("selected")})),$$.dc("wkdl",t).append(gi("compressed","ico")).click({rd:new Date(l)},(function(e){e.stopPropagation();let t=fdt(e.data.rd,"yy-MM-dd");$inv.downloadzip(t,"w")})),l.setDate(l.getDate()+7)}}}n.ldng(0)},rerenderinv:function(){let e=$("#contentframe .invfrm:first");if(e.length>0){let t=e.data("sets")||{};t.mode&&$inv.renderinv(t.tgt,t.mode,t.includes)}},renderinv:function(e,t,n){let i=$fis.frm_list(!0,!0).ldng(1),a=$$.dc("invfrm",i).aC("md"+t).data("sets",$.extend({},{tgt:e,mode:t,includes:n})),r=$fis.lf();$ocms.postXT({url:$ocms.url("inv/invl"),data:{mode:t,tgt:e,includes:n},success:i=>{r.rC("fix").aC("hd"),$$.dc("ovhd",a).text(i.admin.title);let l=$$.tblset({},a),d=$invcol.inv,s=$$.tr(l.hd);$$.th(s);$.each(d.fields||[],((e,t)=>{$$.th(s).text(t.label),"vat"===t.name&&$$.th(s)})),$.each(i.invoices||[],((s,c)=>{let o=$$.tr(l.bdy);o.click((function(){r.rC("fix").aC("hd"),o.toggleClass("selected").siblings().rC("selected").find("td.av").rC("av"),o.find("td.av").rC("av"),!0===o.is(".selected")?$inv.iMn(c):$inv.eM()}));let u=$$.td(o,{class:"raux"});c.hasFile?($$.dc("idl ilbtn",u,{title:$ict.dl+"\n"+c.DocumentName}).append(gi("save-file","ico")).click({id:c.Id},$inv.downloadinv),$$.dc("idl ilbtn",u,{title:$ict.dsp+"\n"+c.DocumentName}).append(gi("eye-open","ico")).click({id:c.Id,typ:"inv"},$inv.jdisp)):!1===c.isFinal&&!0===$fis.isAuth("fds_inv",2)&&$$.dc("idl ilbtn",u,{title:$ict.ed}).append(gi("edit","ico")).click({id:c.Id},$inv.doContInv),$$.dc("iitm ilbtn",u,{title:$ict.sItm}).append(gi("list","ico")).click({id:c.Id},$inv.showitm),$$.dc("iitm ilbtn",u,{title:$ict.sPay}).append(gi("euro","ico")).click({id:c.Id},$inv.showpay),$.each(d.fields||[],((r,l)=>{let d,s,u=$$.td(o).aC(l.dtype);switch("select"===(l.type||"")?u.text((l.url||{})[c[l.name]]||""):u.text(c[l.name]),l.name||""){case"vat":d=$$.sel().appendTo($$.td(o,{class:"vsel"})),s=(i.admin.ust_options||"19,0%;16,0%;0,0%").split(";"),$.each(s,((e,t)=>{$$.opt(t,t).appendTo(d)})),d.click((function(e){e.stopPropagation()})).val(c[l.name]).change().change({frm:a,tgt:e,mode:t,id:c.Id,td:u,includes:n},$inv.setvat),u.toggleClass("hl",c[l.name].substr(0,2)!==s[0].substr(0,2)).click((function(e){e.stopPropagation(),$(this).toggleClass("av")}));break;case"balance":u.aC("sh_"+(c.SollHaben||"").toLowerCase());break;case"invstatus":case"reminderstatus":u.aC(("invstatus"===l.name?"is_":"rs_")+c[l.name])}}))}))},complete:()=>{i.ldng(0)}})},setvat:function(e){let t=$(this),n=e.data||{};$ocms.postXT({url:$ocms.url("inv/setvat"),data:{id:n.id,val:t.val()},success:e=>{n.td.rC("av"),$inv.renderinv(n.tgt,n.mode,n.includes)}})},downloadzip:function(e,t){$(this).empty();window.open($ocms.url("inv/datevzip?mode="+t+"&tgt="+encodeURIComponent(e)),"_blank")},showitm:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&$ocms.postXT({url:$ocms.url("inv/rqi"),data:{id:e.data.id},success:e=>{let t=$$.dc("rfrm");(e.requests||[]).length<1?t.text($ict.nd):$.each(e.requests||[],(function(e,n){let i=$$.dc("srq",t);$$.dc("nme",i).text(n.name);let a=$$.tblset({class:"if"},i);$.each(n.items||[],((e,t)=>{let n=$$.tr({id:"itm"+t.Id}).appendTo(a.bdy);$$.td(n).text(t.NameOrNumber),$$.td(n).text(t.Type),$$.td(n).aC("currency").text(t.net_pos),$$.td(n).aC("currency").text(t.bo_pos),$$.td(n).aC("num").text(t.vat)}))})),$ocms.dlg(t,{width:1e3})}})},showpay:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&$ocms.postXT({url:$ocms.url("inv/pyi"),data:{id:e.data.id},success:e=>{let t=$$.dc("rfrm");if((e.payments||[]).length<1)t.text($ict.nd);else{let n=$$.tblset({class:"if"},t),i=$$.tr(n.hd);$.each(["date","account","name","text","InvoiceID","amount","manual"],((e,t)=>{$$.th(i,$ict.payi[t])})),$.each(e.payments,((e,t)=>{let i=$$.tr({id:"itm"+t.banking_uid}).appendTo(n.bdy);$$.td(i).aC("date").text(t.date),$$.td(i).text(t.account),$$.td(i).text(t.name),$$.td(i).text(t.text),$$.td(i).text(t.InvoiceID),$$.td(i).aC("currency").text(t.amount),$$.td(i).text(t.manual)}))}$ocms.dlg(t,{width:1e3,title:"Übersicht der Zahlungen"})}})},downloadinv:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&window.open($ocms.url("inv/rdoc?id="+e.data.id),"_blank")},doContInv:function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&$inv.cntInv({id:e.data.id})}},$$inv={init2:$inv.init2,auth:{}};export default $$inv;$inv.cInv=function(e){let t=$(this).closest("tr");e.stopPropagation(),!1!==t.is(".selected")&&!1!==$fis.isAuth("fds_inv",2)&&$inv.cInv2({id:e.data.id})},$inv.rMn=e=>{let t=[{lbl:$ict.req,itm:[]}];return!0===bool(e,!1)&&!0===$fis.isAuth("fds_inv",2)&&Array.prototype.push.apply(t[0].itm,[{lbl:$rct.crI,fnc:$inv.ccInv,data:{typ:"r"}},{lbl:$rct.crII,fnc:$inv.ccInv,data:{typ:"i"}}]),t.push({lbl:$ict.rel,fnc:$inv.rReload}),$("#topbar").ocmsmenu(t)},$inv.iMnr=e=>{let t=booln(e.isFinal,!0),n=e.Id,i=booln(e.fds,!1),a=[{glyph:"glyphicon-menu-left",fnc:()=>{$fis.frm_edit().remove()}},{lbl:$ict.inv,itm:[]},{lbl:$ict.rem,itm:[]}];return!1===t&&!0===$fis.isAuth("fds_inv",2)?a[1].itm.push({lbl:$ict.ced,fnc:$inv.clCntInv}):!0===$fis.isAuth("fds_inv",1)&&a[1].itm.push({lbl:$ict.dsp,fnc:()=>$inv.disp(n,"inv")}),!0===i&&!0===t&&!0===$fis.isAuth("fds_inv",2)&&(a[1].itm.push({lbl:$ict.storno,fnc:()=>$inv.storno(n,i)}),a[1].itm.push({lbl:$ict.credit,fnc:()=>$inv.credit(n,i)})),!0===t&&!0===$fis.isAuth("fds_reminder",2)&&(a[2].itm.push({lbl:$ict.remd,fnc:()=>$inv.ccRem(n,e.InvoiceId)}),a[2].itm.push({lbl:$ict.remlst,fnc:()=>$inv.dspRem(n)})),!0===t&&!0===$fis.isAuth("fds_reminder",2)&&!1===booln(e.IsSent,!1)&&a[2].itm.push({lbl:$ict.srs,fnc:()=>$inv.srs(n)}),a.push({lbl:$ict.rel,fnc:$inv.rReload}),$("#topbar").ocmsmenu(a)},$inv.iMn=e=>{let t=booln(e.isFinal,!0),n=e.Id,i=booln(e.fds,!1),a=[{glyph:"glyphicon-menu-left",fnc:()=>{$fis.frm_edit().remove()}},{lbl:$ict.inv,itm:[]},{lbl:$ict.rem,itm:[]}];return!1===t&&!0===$fis.isAuth("fds_inv",2)?a[1].itm.push({lbl:$ict.ced,fnc:()=>{$inv.cntInv({id:n})}}):!0===$fis.isAuth("fds_inv",1)&&a[1].itm.push({lbl:$ict.dsp,fnc:()=>$inv.disp(n,"inv")}),!0===$fis.isAuth("fds_inv",2)&&(a[1].itm.push({lbl:$ict.storno,fnc:()=>$inv.storno(n,i)}),a[1].itm.push({lbl:$ict.credit,fnc:()=>$inv.credit(n,i)})),!0===t&&!1===booln(e.IsPayed,!1)?(!0===$fis.isAuth("fds_reminder",2)&&a[2].itm.push({lbl:$ict.remd,fnc:()=>$inv.ccRem(n,e.InvoiceId)}),!0===$fis.isAuth("fds_inv",2)&&a[1].itm.push({lbl:$ict.setpyd,fnc:()=>$inv.setPyd(n)})):!0===t&&!0===booln(e.IsPayed,!1)&&"m"===(e.PaymentStatus||"")&&!0===$fis.isAuth("fds_inv",2)&&a[1].itm.push({lbl:$ict.setupd,fnc:()=>$inv.setUpd(n)}),!0===$fis.isAuth("fds_reminder",2)&&a[2].itm.push({lbl:$ict.remlst,fnc:()=>$inv.dspRem(n)}),!0===t&&!0===$fis.isAuth("fds_inv",2)&&!1===booln(e.IsSent,!1)&&a[1].itm.push({lbl:$ict.sis,fnc:()=>$inv.sis(n)}),!1===i&&a[1].itm.push({lbl:$ict.mfr,fnc:()=>$inv.mfrrel(n)}),$("#topbar").ocmsmenu(a)},$inv.emOpt=e=>"iss,p13b,ctp"+(""===((e||{}).setmode||"")?",setm":""),$inv.eM=(e,t,n)=>{let i=[];return!0!==booln(e,!1)&&!0!==booln(t,!1)||i.push({glyph:"glyphicon-menu-left",fnc:()=>{$fis.lf(!0),$fis.frm_edit().remove()}}),!0===(n||"").split(",").includes("iss")&&i.push({lbl:$ict.iss,fnc:$inv.ssave}),!0===(n||"").split(",").includes("ctp")&&i.push({lbl:$ict.ctp,fnc:$inv.sctp}),!0===(n||"").split(",").includes("p13b")&&i.push({lbl:$ict.p13b,fnc:$inv.sp13b}),!0===(n||"").split(",").includes("setm")&&i.push({lbl:$ict.setm,fnc:$inv.ssetmode}),!0===(n||"").split(",").includes("iss")&&(i.push({lbl:"Änderungshistorie",fnc:()=>$inv.d.history()}),i.push({lbl:"Änderungen verwerfen",fnc:()=>$inv.d.discard()})),!0===booln(e,!1)&&i.push({lbl:$ict.rel,fnc:$inv.rReload}),$("#topbar").ocmsmenu(i)},$inv.d={tbl:()=>$("div.invoice_layout table.invi"),layout:()=>$("div.invoice_layout"),token:function(){return $inv.d.tbl().data("dtoken")||""},hashes:function(){let e=$inv.d.tbl().data("bai")||[],t={};return $.each(e,((e,n)=>{t[(n.Id||"").toString()]=JSON.stringify(n)})),t},seed:function(e){let t=$inv.d.layout();t.aC("freeze");let n=$ocms.postXT({url:$ocms.url("inv/dopen"),data:{payload:JSON.stringify(e)},success:e=>{$inv.d.tbl().data("dtoken",e.token).data("dver",e.version).data("dhashes",$inv.d.hashes()).data("dorder",$inv.d.order()),$fis.draft.bind(e.token,{onReady:()=>$inv.d.refresh(),onExpiring:e=>$inv.d.warnExpiry(e),onClosed:e=>$inv.d.closed(e)}),$inv.d.refresh()},error:()=>{t.rC("freeze")},complete:()=>{$inv.d.tbl().removeData("dseeding").removeData("dseedpromise")}});return $inv.d.tbl().data("dseedpromise",n),n},refresh:function(e){let t=$inv.d.token();""!==t&&$ocms.postXT({url:$ocms.url("inv/dstate"),data:{token:t},success:t=>{$inv.d.applyState(t),"function"==typeof e&&e(t)},error:e=>{e&&410===e.status&&$inv.d.closed("expired")},complete:()=>{$inv.d.layout().rC("freeze")}})},applyState:function(e){let t=$inv.d.tbl();if(t.length<1)return;let n=t.data("dver");"number"==typeof n&&"number"==typeof e.version&&e.version$$.dc("dnote").aC(e.style||"InvoiceNotes").text(e.text)))},applyItems:function(e,t){let n=e=>void 0===e?null:e;(t||[]).forEach((t=>{if(!t)return;let i=(t.Id||"").toString();if(""===i)return;let a=e.children("tbody").filter(((e,t)=>(($(t).data()||{}).Id||"").toString()===i)).first();if(a.length<1)return;let r=t.itm||[],l={},d=a.find("tr.title").first(),s=d.length?d:null;r.forEach((e=>{let t=(e.id||"").toString();if(""===t)return;l[t]=!0;let i=a.find("#itm"+t).first(),r=i.length<1;r&&(i=$$.tr(a,{id:"itm"+t,class:"itm"}));let d=i.data()||{},c=r||(d.Type||"")!==(e.typ||"")||d.net!==n(e.v)||d.net_val!==n(e.vt)||d.vat_val!==n(e.vv)||d.svcnet_val!==n(e.vs)||d.svcvat_val!==n(e.vsv)||(d.position||"")!==(null!=e.p?e.p:"");$.extend(d,{Id:t,Type:e.typ,net:n(e.v),net_val:n(e.vt),vat_val:n(e.vv),svcnet_val:n(e.vs),svcvat_val:n(e.vsv),vat:e.vat,position:null!=e.p?e.p:"",SetItmId:null!=e.SetItmId?e.SetItmId:null}),r&&$.extend(d,{htmltext:e.t,quantity:e.q}),i.data(d),s?i.prev()[0]!==s[0]&&i.insertAfter(s):a.children().first()[0]!==i[0]&&a.prepend(i),s=i,c&&$inv.rrw.call(i)})),a.find("tr.itm").each(((e,t)=>{let n=(($(t).data()||{}).Id||"").toString();""===n||l[n]||$(t).remove()}))}))},applySetDisplay:function(e,t){e.find("tr.itm").each(((e,n)=>{let i=$(n),a=((i.data()||{}).Id||"").toString();if(""===a||!Object.prototype.hasOwnProperty.call(t,a))return;let r=t[a]||{};i.tC("sethdr",bool(r.h,!1)),!1===bool(r.p,!0)&&i.find("td.currency").text("")}))},sync:function(e){let t=$inv.d.token();if(""===t){let t=$inv.d.tbl().data("dseedpromise");return t?$.when(t).then((()=>$inv.d.sync(e))):$.when()}return $inv.d.layout().aC("freeze"),$ocms.postXT({url:$ocms.url("inv/dpatch"),data:{token:t,delta:JSON.stringify(e)},success:()=>{$inv.d.refresh()},error:e=>{$inv.d.layout().rC("freeze"),e&&410===e.status&&$inv.d.closed("expired")}})},order:function(){return($inv.d.tbl().data("bai")||[]).map((e=>(e.Id||"").toString()))},syncChanged:function(e){let t=e.data("bai")||[],n=e.data("dhashes")||{},i={},a=[],r=[],l=[];$.each(t,((e,t)=>{let r=(t.Id||"").toString(),l=JSON.stringify(t);i[r]=l,n[r]!==l&&a.push(t)})),$.each(n,(e=>{void 0===i[e]&&r.push(e)}));let d=$inv.d.order(),s=e.data("dorder")||[];return e.data("dhashes",i).data("dorder",d),a.forEach((e=>l.push($inv.d.sync({Target:"block.replace",Ref:(e.Id||"").toString(),Value:e})))),r.forEach((e=>l.push($inv.d.sync({Target:"block.remove",Ref:e})))),s.length===d.length&&s.slice().sort().join(",")===d.slice().sort().join(",")&&s.join(",")!==d.join(",")&&l.push($inv.d.sync({Target:"block.order",Value:d})),$.when.apply($,l).promise()},syncField:function(e,t){let n={invoicetitle:"title",invoiceaddress:"address",invoiceemail:"email",loc:"provisionlocation",provisionlocation:"provisionlocation",provisionperiod:"provisionperiod"}[e];n&&$inv.d.sync({Target:n,Value:t})},footer:function(e,t,n){let i=e.children("tfoot").empty();e.nextAll(".fnote").remove();let a=bool(n.p13b,!1),r=(e,t,n)=>$$.tdc("currency",$$.tr(i,{class:n||"tsum"}).append([$$.tdc("aux"),$$.td({colspan:4}).text(e)]),fnum(t,$rct.cst)),l=t=>$$.dc("fnote").insertAfter(e).rwText(t);r("Netto",t.total_net||0),!1===a&&$.each(t.vat||{},((e,t)=>r($rct.vat+" "+e+"%",t,"tvat"))),r("Summe",t.total_gross||0);let d=n.type||"";"i"===d?(l($rct.note2),l($rct.note4)):"c"===d?l($rct.note2):(l(string($rct.note3,[fnum(((t.service_net||0)+(t.service_vat||0))*(n.tax_servicerefund||0),$rct.cst)])).aC("ntax"),l($rct.note2),l(string($rct.note1,[fnum((t.service_net||0)+(t.service_vat||0),$rct.cst),fnum(t.service_net||0,$rct.cst),fnum(t.service_vat||0,$rct.cst)]))),!0===a&&l($rct.note13b)},validation:function(e){let t=$("div.invoice_layout");if(t.length<1)return;let n=t.children(".dvalidation");n.length<1&&(n=$$.dc("dvalidation"),t.prepend(n)),n.empty().tC("hidden",(e||[]).length<1),$.each(e||[],((e,t)=>$$.dc("dvmsg",n).aC(t.severity).text(t.message)))},preview:function(){let e=$inv.d.token();if(""===e)return;let t=$inv.d.layout(),n=($inv.d.tbl().data("new")||{}).invoiceemail||"";!1===$fis.ValidateEmail(n)&&!1===bool(confirm($ict.ivE+$ict.ivEc),!1)||(t.aC("freeze"),$ocms.postXT({url:$ocms.url("inv/dpreview"),data:{token:e},success:n=>{t.rC("freeze");let i=$$.dc("imagecollection pdfpreview"),a=Math.round(.88*vh()),r=n.total;r>10&&$$.dc("note warn",i).text($ict.tpe),$.each(n.img||[],((e,t)=>{$$.dc("pdfp",i).append($$.img(t).css("max-height",(a-rpx(6)).toString()+"px"))}));for(let e=(n.img||[]).length+1;e<=r;e++)$$.dc("pdfp ph",i).append($$.dc("note",$ict.pna));$ocms.dlg(i,{size:[a,Math.round(.88*vw())],zindex:50,form:!1,button:$rct.crI,confirm:function(n){let i=$(this);t.aC("freeze"),$ocms.postXT({url:$ocms.url("inv/dsave"),data:{token:e},success:e=>{$ocms.postXT({url:$ocms.url("req/sconf"),data:{id:e.invid},success:t=>{i.trigger("modal_close"),!0===t.hasFile&&window.open($ocms.url("req/idoc")+"?id="+e.invid,"_blank"),$inv.d.close(),$ocms.init("req"),$inv.rReload()},error:()=>{alert($t.f1),i.trigger("modal_close")},complete:()=>{t.rC("freeze")}})},error:()=>{t.rC("freeze"),alert($ict.eis)}})},cancel:function(e){confirm($ict.cdI)&&($inv.d.close(),$inv.rReload())}})},error:()=>{t.rC("freeze"),alert($ict.eis)}}))},save:function(){let e=$inv.d.token();if(""===e)return;let t=$inv.d.layout();t.aC("freeze"),$ocms.postXT({url:$ocms.url("inv/dsave"),data:{token:e},success:e=>{$inv.d.tbl().data("invid",e.invid)},error:()=>{alert($ict.eis)},complete:()=>{t.rC("freeze")}})},history:function(){let e=$inv.d.token();""!==e&&$ocms.postXT({url:$ocms.url("inv/dhistory"),data:{token:e},success:e=>{let t=$$.dc("dhist");if((e.history||[]).length<1)$$.dc("note",t).text("Noch keine Änderungen erfasst.");else{let n=$$.tblset({class:"invtbl fullwidth"},t);$$.tr(n.hd).append([$$.th().text("Zeit"),$$.th().text("Feld"),$$.th().text("Alt"),$$.th().text("Neu")]),$.each(e.history,((e,t)=>$$.tr(n.bdy).append([$$.tdc("keep",fdt(t.timestamp)),$$.td().text(t.target),$$.td().text(t.oldValue),$$.td().text(t.newValue)])))}$ocms.dlg(t,{width:800,form:!1})}})},discard:function(){let e=$inv.d.tbl().data("invid")||"";""!==e?!1!==confirm("Alle Änderungen verwerfen und den zuletzt gespeicherten Stand neu laden?")&&($inv.d.close(),$inv.cntInv({id:e})):alert("Es wurde noch kein Zwischenstand gespeichert, der wiederhergestellt werden könnte.")},warnExpiry:function(e){let t=Math.max(1,Math.round((e||0)/60));$fis.notifications.push({severity:"info",title:"Entwurf läuft ab",message:"Der Rechnungsentwurf läuft in etwa "+t+" Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren."})},closed:function(e){let t=$inv.d.token();$inv.d.tbl().removeData("dtoken"),""!==t&&$fis.draft.release(t),$fis.frm_edit().remove(),$fis.lf(!0),$fis.notifications.push({severity:"error",title:"Entwurf geschlossen",message:"expired"===e?"Der Rechnungsentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.":"Der Rechnungsentwurf wurde geschlossen."});try{$inv.rReload()}catch(e){}},close:function(){let e=$inv.d.token();""!==e&&($ocms.postXT({url:$ocms.url("inv/dclose"),data:{token:e}}),$fis.draft.release(e)),$inv.d.tbl().removeData("dtoken")}},$inv.rd={tbl:()=>$("div.invoice_layout table.invi"),layout:()=>$("div.invoice_layout"),token:function(){return $inv.rd.tbl().data("rdtoken")||""},seed:function(e){let t=$inv.rd.layout();t.aC("freeze"),$ocms.postXT({url:$ocms.url("rem/dopen"),data:{payload:JSON.stringify(e)},success:e=>{$inv.rd.tbl().data("rdtoken",e.token).data("rdver",e.version),$fis.draft.bind(e.token,{onReady:()=>$inv.rd.refresh(),onExpiring:e=>$inv.rd.warnExpiry(e),onClosed:e=>$inv.rd.closed(e)}),$inv.rd.refresh()},error:()=>{t.rC("freeze")}})},refresh:function(e){let t=$inv.rd.token();""!==t&&$ocms.postXT({url:$ocms.url("rem/dstate"),data:{token:t},success:t=>{$inv.rd.applyState(t),"function"==typeof e&&e(t)},error:e=>{e&&410===e.status&&$inv.rd.closed("expired")},complete:()=>{$inv.rd.layout().rC("freeze")}})},applyState:function(e){let t=$inv.rd.tbl();if(t.length<1)return;let n=t.data("rdver");"number"==typeof n&&"number"==typeof e.version&&e.version{let t=$$[0]("p").aC("dnote intro").rwText(e);a.length?t.insertBefore(a):t.appendTo(n)})),((e||{}).closing||[]).forEach((e=>$$[0]("p").aC("dnote closing").rwText(e).appendTo(n)))},sync:function(e){let t=$inv.rd.token();""!==t&&($inv.rd.layout().aC("freeze"),$ocms.postXT({url:$ocms.url("rem/dpatch"),data:{token:t,delta:JSON.stringify(e)},success:()=>{$inv.rd.refresh()},error:e=>{$inv.rd.layout().rC("freeze"),e&&410===e.status&&$inv.rd.closed("expired")}}))},syncField:function(e,t){if(""===$inv.rd.token())return;let n={subject:"subject",invoiceaddress:"address",invoiceemail:"email",text:"text"}[e];n&&$inv.rd.sync({Target:n,Value:t})},syncAmount:function(e,t){""!==$inv.rd.token()&&($inv.rd.sync({Target:"amount",Value:(null!=e?e:0).toString()}),$inv.rd.sync({Target:"amount_payed",Value:(null!=t?t:0).toString()}))},footer:function(e,t){let n=e.children("tfoot").empty(),i=$$.tr(n,{class:"tsum"}).append([$$.tdc("aux"),$$.td({colspan:4}).text("Offener Betrag")]);$$.tdc("currency",i,fnum(t.amount_open||0,$rct.cst))},validation:function(e){let t=$inv.rd.layout();if(t.length<1)return;let n=t.children(".dvalidation");n.length<1&&(n=$$.dc("dvalidation"),t.prepend(n)),n.empty().tC("hidden",(e||[]).length<1),$.each(e||[],((e,t)=>$$.dc("dvmsg",n).aC(t.severity).text(t.message)))},preview:function(){let e=$inv.rd.token();if(""===e)return;let t=$inv.rd.layout(),n=($inv.rd.tbl().data("new")||{}).invoiceemail||"";!1===$fis.ValidateEmail(n)&&!1===bool(confirm($ict.ivE+$ict.ivEc),!1)||(t.aC("freeze"),$ocms.postXT({url:$ocms.url("rem/dpreview"),data:{token:e},success:n=>{t.rC("freeze");let i=$$.dc("imagecollection pdfpreview"),a=Math.round(.88*vh());$.each(n.img||[],((e,t)=>{$$.dc("pdfp",i).append($$.img(t).css("max-height",(a-rpx(6)).toString()+"px"))})),$ocms.dlg(i,{size:[a,Math.round(.88*vw())],zindex:50,form:!1,button:$ict.remd,confirm:function(n){let i=$(this);t.aC("freeze"),$ocms.postXT({url:$ocms.url("rem/dsave"),data:{token:e},success:e=>{$ocms.postXT({url:$ocms.url("rem/conf"),data:{id:e.remid},success:()=>{i.trigger("modal_close"),window.open($ocms.url("rem/idoc")+"?id="+e.remid,"_blank"),$inv.rd.close(),$ocms.init("req"),$inv.rReload()},error:()=>{alert($t.f1),i.trigger("modal_close")},complete:()=>{t.rC("freeze")}})},error:()=>{t.rC("freeze"),alert($t.f1)}})},cancel:function(e){confirm($ict.cdI)&&($inv.rd.close(),$inv.rReload())}})},error:()=>{t.rC("freeze"),alert($t.f1)}}))},save:function(){let e=$inv.rd.token();if(""===e)return;let t=$inv.rd.layout();t.aC("freeze"),$ocms.postXT({url:$ocms.url("rem/dsave"),data:{token:e},success:e=>{$inv.rd.tbl().data("remid",e.remid)},error:()=>{alert($t.f1)},complete:()=>{t.rC("freeze")}})},history:function(){let e=$inv.rd.token();""!==e&&$ocms.postXT({url:$ocms.url("rem/dhistory"),data:{token:e},success:e=>{let t=$$.dc("dhist");if((e.history||[]).length<1)$$.dc("note",t).text("Noch keine Änderungen erfasst.");else{let n=$$.tblset({class:"invtbl fullwidth"},t);$$.tr(n.hd).append([$$.th().text("Zeit"),$$.th().text("Feld"),$$.th().text("Alt"),$$.th().text("Neu")]),$.each(e.history,((e,t)=>$$.tr(n.bdy).append([$$.tdc("keep",fdt(t.timestamp)),$$.td().text(t.target),$$.td().text(t.oldValue),$$.td().text(t.newValue)])))}$ocms.dlg(t,{width:800,form:!1})}})},warnExpiry:function(e){let t=Math.max(1,Math.round((e||0)/60));$fis.notifications.push({severity:"info",title:"Entwurf läuft ab",message:"Der Mahnentwurf läuft in etwa "+t+" Minute(n) ab. Bitte zwischenspeichern, sonst gehen die Änderungen verloren."})},closed:function(e){let t=$inv.rd.token();$inv.rd.tbl().removeData("rdtoken"),""!==t&&$fis.draft.release(t),$fis.frm_edit().remove(),$fis.lf(!0),$fis.notifications.push({severity:"error",title:"Entwurf geschlossen",message:"expired"===e?"Der Mahnentwurf ist wegen Inaktivität abgelaufen. Nicht gespeicherte Änderungen sind verloren.":"Der Mahnentwurf wurde geschlossen."});try{$inv.rReload()}catch(e){}},close:function(){let e=$inv.rd.token();""!==e&&($ocms.postXT({url:$ocms.url("rem/dclose"),data:{token:e}}),$fis.draft.release(e)),$inv.rd.tbl().removeData("rdtoken")}},$inv.cInv2=function(e){let t=$$.dc("rfrm").ldng(1),n=$ocms.dlg(t,{width:1e3});n&&n.ft.rwText($rct.rq1);let i=()=>{$ocms.postXT({url:$ocms.url("req/get"),timeout:60,data:{id:e.id,mode:"r"},success:t=>{t.admin=t.admin||{};let n=$fis.lf(!0).aC("fix").rC("hd");if($fis.frm_edit().IN(),$inv.eM(!0,!0),(t.requests||[]).length<1)n.aC("fix").text($rct.nd);else{$$.dc("lh",n,$rct.mdl);let i=$$.d(),a=$$.ul({class:"rql"}).data({search:e.id,parent:t.admin.parent}).appendTo(n),r={},l=$rcol.req.lbl();$.each(t.requests||[],(function(e,t){let n=$$.li({class:"cli rli"}).data($.extend({},t)).appendTo(a),d=$$.dc("lihd",n).addClass(t.state);!0===booln(t.open,!1)&&d.append($$.sc("cbox").click((()=>{n.tC("checked"),i.find("li").rC("checked"),!0===n.is(".checked")?$inv.rMn(t.open):$inv.eM(!0)}))),d.append([$$.sc("eid",t.ExternalId),$$.sc("nme",t.Name)]),$$.dc("lidt",n).append([$$.dc("rqs").append([$$.s(l.State+": "),$$.s($rct.sts[t.State||"-"])]),$$.dc("ivn").append([$$.s(l.InvoiceId+": "),$$.s(t.InvoiceId||"- -")]),$$.dc("wda").append([$$.s(l.WorkDoneAt+": "),$$.s(fdt(t.WorkDoneAt,"dd.MM.yyyy"))])]),r[t.Id]=n})),(t.inv||[]).length>0&&($$.dc("lh",n,$rct.invs),i=$$.ul({class:"ivl"}).appendTo(n),$.each(t.inv||[],((e,t)=>{let n=$$.li({class:"cli ili"}).data($.extend({},t)).appendTo(i),r=$$.dc("lihd",n).addClass(t.invstatus);!1===booln(t.isFinal,!0)?r.append($$.sc("cbox").click((()=>{""!==(t.Id||"")&&(n.tC("checked").siblings().rC("checked"),a.find("li").rC("checked"),!0===n.is(".checked")?$inv.iMnr(t):$inv.eM(!0))}))):["","dft"].indexOf(t.invstatus)<0&&r.append($$.sc("dli").click((function(){$inv.disp(t.Id,"inv")}))),r.append($$.sc("nme",t.DocumentName||t.Id)),$$.dc("lidt",n).append([$$.dc("wda").append([$$.s(fdt(t.DateCreated,"dd.MM.yyyy"))]),$$.d().text($ict.iSt[t.invstatus]||t.invstatus)])})))}},complete:()=>{n&&n.c.trigger("modal_close")}})};$ocms.postXT({url:$ocms.url("req/pget"),timeout:90,data:{id:e.id},success:e=>{n&&n.ft.rwText($rct.rq2),i()},error:()=>{confirm($rct.rq1f)?(n&&n.ft.rwText($rct.rq2),i()):n&&n.c.trigger("modal_close")}})},$inv.ccInv=function(e){let t=(e.data||{}).typ||"r",n=$fis.lf(),i=n.children("ul.rql"),a=i.data("parent"),r=[];if(i.find("li.rli.checked").each((function(){r.push($(this).data("Id"))})),r.length<1)return void alert($rct.dnS);if("i"===t&&r.length>1)return void alert($rct.dII);let l=$fis.frm_edit(),d=$$.dc("invoice_layout",l).append($$.dc("btn sprev").click($inv.sprev)),s=$fis.cf().width()>d.width()+n.width()+20;n.tC("fix",s).tC("hd",!s),$inv.eM(!1,!0);let c=$$.dc("rfrm").ldng(1),o=$ocms.dlg(c,{width:1e3});o.ft.rwText($rct.rq2),$ocms.postXT({url:$ocms.url("req/iget"),timeout:60,data:{id:a,mode:"ful",typ:t,sel:r.join(",")},success:e=>{let t=$$.dc("srq",d),n=$$.tblset({class:"invi"},t);n.bdy.remove(),n.ft=$$[0]("tfoot"),e.admin=e.admin||{},e.admin.p13b=bool(e.admin.p13b||"",!0===((e.inv||{}).InvoiceOptions||"").split(",").includes("§13b")),n.tbl.data($.extend({new:{},sms:{},itm:{}},{admin:e.admin,companies:e.companies,locations:e.locations}));let i=$$.tr(n.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(i,e))),n.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.requests||[],(function(t,i){if(0!==(i.Id||0)){let t=$inv.worknotes(i);i.text="i"===e.admin.type?$rct.req+jine([i.ExternalId,i.Name],": ").eine(" ",""):jine([jine([fdt(i.WorkDoneAt,"dd.MM.yy"),i.ExternalId]," - "+$rct.req+" "),t.ne(i.Name)],": \n");let a=$$.tbody(n.tbl).data($.extend({},i));$inv.rendersrq.call(a)}}));let a=$$.tr($$.tbody(n.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(a),n.ft.appendTo(n.tbl);let r,l,s=e.admin||{},c=(e,t,i,a,r)=>{let l=$$.dc("inpfrm",d).aC(e).append("string"==typeof a?$$.dc("ahd",a):a>0?$$.dc("ahd",$rct.frm[i]):null),s=$$.dc("content",l).rwText(t);$$.dc("axf",l).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:s,nme:i,change:e=>{n.tbl.data("new")[i]=e}},r),$inv.eHtml)),n.tbl.data("new")[i]=t};c("tfrm",s.invoicetitle,"invoicetitle",0,null),c("adrfrm",s.invoiceaddress,"invoiceaddress",0,null),c("locfrm","","loc",1,{list:deepCopy(e.locations),lbl:"ref",property:"address"}),c("emailfrm",s.invoiceemail,"invoiceemail",0,null),$$.dc("sndfrm",d).append($$.dc("content").text(s.sender)),s.provisionend&&(l=s.provisionstart?$rct.provP:$rct.provD,r=s.provisionstart?fdt(s.provisionstart,"dd.MM.yyyy")+" - "+fdt(s.provisionend,"dd.MM.yyyy"):fdt(s.provisionend,"dd.MM.yyyy")),c("admfrm",r,"provisionperiod",l,1),n.tbl.data("new").CustomValues=s.CustomValues||"",$$.dc("inpfrm ctpfrm",d).text(jObj(s.CustomValues,"contactName")),n.tbl.children("tbody").each($inv.bdysort),n.tbl.trigger("fds.inv"),$inv.eM(!1,!0,$inv.emOpt(s))},complete:()=>{o.c.trigger("modal_close")}})},$inv.ccStInv=function(e){let t=e.data||{},n=$fis.lf(),i=t.id,a=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.sprev)),r=$fis.cf().width()>a.width()+n.width()+20;n.tC("fix",r).tC("hd",!r),$inv.eM(!1,!0);let l=$$.dc("rfrm").ldng(1),d=$ocms.dlg(l,{width:1e3});d.ft.rwText($ict.iq1),$ocms.postXT({url:$ocms.url("inv/pget"),timeout:90,data:{id:t.id},success:e=>{d&&d.ft.rwText($ict.iq2),$ocms.postXT({url:$ocms.url("inv/icget"),timeout:60,data:{id:i},success:e=>{let t=$$.dc("srq",a),n=$$.tblset({class:"invi"},t);n.bdy.remove(),n.ft=$$[0]("tfoot"),e.admin=e.admin||{},e.admin.p13b=bool(e.admin.p13b||"",!0===((e.inv||{}).InvoiceOptions||"").split(",").includes("§13b")),n.tbl.data($.extend({new:{},sms:{},itm:{}},{admin:e.admin,companies:e.companies,locations:e.locations}));let i=$$.tr(n.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(i,e))),n.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.requests||[],(function(t,i){if(0!==(i.Id||0)){let t=$inv.worknotes(i);i.text="i"===e.admin.type?$rct.req+jine([i.ExternalId,i.Name],": ").eine(" ",""):jine([fdt(i.WorkDoneAt,"dd.MM.yy")+t.ne(i.Name)],": ");let a=$$.tbody(n.tbl).data($.extend({},i));$inv.rendersrq.call(a)}}));let r=$$.tr($$.tbody(n.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(r),n.ft.appendTo(n.tbl);let l,d,s=e.admin||{},c=(e,t,i,r,l)=>{let d=$$.dc("inpfrm",a).aC(e).append("string"==typeof r?$$.dc("ahd",r):r>0?$$.dc("ahd",$rct.frm[i]):null),s=$$.dc("content",d).rwText(t);$$.dc("axf",d).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:s,nme:i,change:e=>{n.tbl.data("new")[i]=e}},l),$inv.eHtml)),n.tbl.data("new")[i]=t};c("tfrm",s.invoicetitle,"invoicetitle",0,null),c("adrfrm",s.invoiceaddress,"invoiceaddress",0,null),c("locfrm",s.provisionlocation,"loc",1,{list:deepCopy(e.locations),lbl:"ref",property:"address"}),c("emailfrm",s.invoiceemail,"invoiceemail",0,null),$$.dc("sndfrm",a).append($$.dc("content").text(s.sender)),s.provisionend&&(d=s.provisionstart?$rct.provP:$rct.provD,l=s.provisionstart?fdt(s.provisionstart,"dd.MM.yyyy")+" - "+fdt(s.provisionend,"dd.MM.yyyy"):fdt(s.provisionend,"dd.MM.yyyy")),c("admfrm",l,"provisionperiod",d,1),n.tbl.data("new").CustomValues=s.CustomValues||"",$$.dc("inpfrm ctpfrm",a).text(jObj(s.CustomValues,"contactName")),n.tbl.children("tbody").each($inv.bdysort),n.tbl.trigger("fds.inv")},complete:()=>{d.c.trigger("modal_close")}})},error:()=>{d&&d.c.trigger("modal_close")}})},$inv.clCntInv=function(e){let t=$fis.lf(!1),n=[];t.find("li.ili.checked").each((function(){n.push($(this).data("Id"))})),1===n.length&&$inv.cntInv({id:n[0]})},$inv.cntInv=function(e){e=e||{};$fis.lf(!1).rC("fix").aC("hd");let t=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.sedit));$inv.eM(!1,!0);let n=$$.dc("rfrm").ldng(1),i=$ocms.dlg(n,{width:1e3});i.ft.rwText($rct.rq2),$ocms.postXT({url:$ocms.url("inv/get"),timeout:60,data:{id:e.id},success:e=>{e.admin=e.admin||{};let n=e.inv||{},i=$$.dc("srq",t),a=$$.tblset({class:"invi"},i);a.bdy.remove(),a.ft=$$[0]("tfoot"),a.tbl.data($.extend({invid:n.Id,new:{},sms:{},itm:{},bai:[]},e));let r=$$.tr(a.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(r,e))),a.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.req||[],(function(e,t){let n=$$.tbody(a.tbl).data($.extend({},t));$inv.rendersrq.call(n)}));let l=$$.tr($$.tbody(a.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(l),a.ft.appendTo(a.tbl);let d=(e,n,i,r,l)=>{let d=$$.dc("inpfrm",t).aC(e).append("string"==typeof r?$$.dc("ahd",r):r>0?$$.dc("ahd",$rct.frm[i]):null),s=$$.dc("content",d).rwText(n);$$.dc("axf",d).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:s,nme:i,change:e=>{a.tbl.data("new")[i]=e}},l),$inv.eHtml)),a.tbl.data("new")[i]=n};d("tfrm",n.InvoiceTitle,"invoicetitle",0,null),d("adrfrm",n.SendToAddress,"invoiceaddress",0,null),d("locfrm",n.ProvisionLocation,"loc",1,null),d("emailfrm",n.SendToEmail,"invoiceemail",0,null),$$.dc("sndfrm",t).append($$.dc("content").text(e.admin.sender)),d("admfrm",n.ProvisionPeriod,"provisionperiod",!0===(n.ProvisionPeriod||"").includes("-")?$rct.provP:$rct.provD,1),a.tbl.data("new").CustomValues=n.CustomValues||"",$$.dc("inpfrm ctpfrm",t).text(jObj(n.CustomValues,"contactName")),a.tbl.children("tbody").each($inv.bdysort),a.tbl.trigger("fds.inv"),$inv.eM(!1,!0,$inv.emOpt(e.admin))},complete:()=>{i.c.trigger("modal_close")}})},$inv.cSt=function(e){e=e||{};let t=$fis.lf(),n=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.sedit)),i=$fis.cf().width()>n.width()+t.width()+20;t.tC("fix",i).tC("hd",!i),$inv.eM(!1,!0);let a=$$.dc("rfrm").ldng(1),r=$ocms.dlg(a,{width:1e3});r.ft.rwText($ict.iq1),$ocms.postXT({url:$ocms.url("inv/pget"),data:{id:e.id},success:t=>{r&&r.ft.rwText($ict.iq2),$ocms.postXT({url:$ocms.url("inv/storno"),data:{id:e.id,mode:e.mode},success:e=>{e.admin=e.admin||{},e.admin.p13b=bool(e.admin.p13b||"",!0===((e.inv||{}).InvoiceOptions||"").split(",").includes("§13b"));let t=e.inv||{},i=$$.dc("srq",n),a=$$.tblset({class:"invi"},i);a.bdy.remove(),a.ft=$$[0]("tfoot"),a.tbl.data($.extend({invid:t.Id,new:{},sms:{},itm:{},bai:[]},e));let r=$$.tr(a.hd).aC("shd").append([$$.th().aC("aux")]);$rct.invHR.forEach((e=>$$.th(r,e))),a.tbl.on("fds.inv",$inv.invSumUpdate),$.each(e.req||[],(function(e,t){let n=$$.tbody(a.tbl).data($.extend({},t));$inv.rendersrq.call(n)}));let l=$$.tr($$.tbody(a.tbl),{class:"placeholder"}).data({net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0});$inv.rrw.call(l),a.ft.appendTo(a.tbl);let d=(e,t,i,r,l)=>{let d=$$.dc("inpfrm",n).aC(e).append("string"==typeof r?$$.dc("ahd",r):r>0?$$.dc("ahd",$rct.frm[i]):null),s=$$.dc("content",d).rwText(t);$$.dc("axf",d).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:s,nme:i,change:e=>{a.tbl.data("new")[i]=e}},l),$inv.eHtml)),a.tbl.data("new")[i]=t};d("tfrm",t.InvoiceTitle,"invoicetitle",0,null),d("adrfrm",t.SendToAddress,"invoiceaddress",0,null),d("locfrm",t.ProvisionLocation,"loc",1,null),d("emailfrm",t.SendToEmail,"invoiceemail",0,null),$$.dc("sndfrm",n).append($$.dc("content").text(e.admin.sender)),d("admfrm",t.ProvisionPeriod,"provisionperiod",!0===(t.ProvisionPeriod||"").includes("-")?$rct.provP:$rct.provD,1),a.tbl.data("new").CustomValues=t.CustomValues||"",$$.dc("inpfrm ctpfrm",n).text(jObj(t.CustomValues,"contactName")),a.tbl.children("tbody").each($inv.bdysort),a.tbl.trigger("fds.inv")},complete:()=>{r.c.trigger("modal_close")}})},error:()=>{r&&r.c.trigger("modal_close")}})},$inv.eHtml=function(e){let t=$(this),n=e.data instanceof jQuery?e.data:e.data.t,i=["invoiceemail","provisionperiod","invoicetitle"].includes(e.data.nme),a=i?[{name:"txt",label:"Text",type:"text",value:n.text()}]:[{name:"txt",label:"Text",type:"html",value:n.html(),tinymce:!0,attr:{style:"height: 300px"}}],r=e.data.change||null,l={title:t.data("dialog")||"",success:function(t){i?n.text(t.txt||""):n.html(t.txt),"function"==typeof r&&r(t.txt),$inv.d.syncField(e.data.nme,i?t.txt||"":t.txt),$inv.rd.syncField(e.data.nme,i?t.txt||"":t.txt)},tinymce:{valid_elements:"br",hidemenu:!0,hidetoolbar:!0}};if(Array.isArray(e.data.list)){let t=$$.dc("lstfrm");$.each(e.data.list,((n,i)=>{let a=$$.dc("li",t).append(""!==(e.data.lbl||"")?$$.dc("lbl").rwText(i[e.data.lbl]):null);$$.dc("adr",a).rwText(i[e.data.property]).data("val",i[e.data.property]).click((function(){let e=$(this),t=e.closest(".modal-body").find(':input[name="txt"]');t.is(".tinymce")?tinymce.get(t.attr("id")).setContent($$.s().rwText(e.data("val")).html()):"TEXTAREA"===t.prop("tagName")?t.val(e.data("val")).change():t.rwText(e.data("val"))}))})),l.addcontent=t}$ocms.dlgform(a,l)},$inv.setVat=function(e){$(this);let t=e.data,n=prompt($rct.rqV);n&&(n=parseFloat(n.replace("%","")),n>1&&(n*=.01),!1===isNaN(n)&&(t.siblings(".itm").each((function(){$(this).data().vat=fnum(n,{style:"percent"}).replace(" ","")})),$inv.t_fds_inv()))},$inv.inRow=function(e){let t=$(this),n=e.data,i={},a=$rcol.itm.clone(["SortOrder","NameOrNumber","Type","quantityhours","UnitString","net","svcnet_val","svcvat_val","net_val","vat_val","vat","Note"]),r="N"+(65536*(1+Math.random())||0).toString(16).substr(6),l=$$.tr({id:"itm_"+r.toString(),class:"itm"});$ocms.dlgform(a,{title:t.data("dialog")||"",success:function(e){l.data($.extend({Id:r},i,e)),$inv.rrw.call(l),l.insertAfter(n),$inv.t_fds_inv()},typedvalues:!0})},$inv.eRow=function(e){let t=$(this),n=e.data,i=n.data()||{},a=["SortOrder","NameOrNumber","Type","quantityhours","UnitString","net","svcnet_val","svcvat_val","net_val","vat_val","vat","Note"];i.id||""!==(i.Type||"")||a.unshift("Type");let r=$rcol.itm.clone(a).applyValues(i);r.set("Type","hidden","type"),$inv.eRw.call(t,n,i,r)},$inv.eRw=function(e,t,n){let i=$(this);$ocms.dlgform(n,{title:i.data("dialog")||"",success:function(n){let i={};""===(t.Id||"")&&(i.Id="N"+(65536*(1+Math.random())||0).toString(16).substr(6),e.attr("id","itm_"+i.Id.toString())),i.quantity=((n.quantityhours||"").toString()+" "+(n.UnitString||"").toString()).trimEnd(),e.data($.extend({},t,n,i)),console.debug("eRw success %o",e.data()),$inv.rrw.call(e),$inv.t_fds_inv()},typedvalues:!0})},$inv.bdysort=(e,t)=>{$(t).Sortable({dragItem:!1,dragHandleClass:"ico",parentident:"tr",onend:()=>{$inv.t_fds_inv()}})},$inv.rrw=function(){let e=$(this),t=e.data(),n={},i=e.is(".placeholder"),a=e.is(".hidenote"),r=e=>$$.d().append(e).html(),l=[$$.dc("ibtn insb",{title:$rct.iRb}).append(gi("indent-left")).click(e,$inv.inRow)];!1===i&&(l.unshift($$.dc("ibtn edit",{title:$rct.cP}).append(gi("pencil")).click(e,$inv.eRow)),l.push($$.dc("ibtn del",{title:$rct.dR}).append(gi("trash")).click((function(t){confirm($rct.cD)&&(e.remove(),$inv.t_fds_inv())}))),"set"===(t.Type||"").toLowerCase()&&0===(t.net_val||0)&&l.push($$.dc("ibtn tosetp",{title:$rct.toSetP}).append(gi("scale")).click(e,$inv.toSetPrice)));let d=$$.dc("axf").append(l);!0===i?n={id:"",typ:"placeholder"}:!0===e.is(".itm.osum")?n={invrqid:t.InvRqId,id:"osum"+e.index(),typ:"osum",p:"",q:null,t:r(t.tbl.tbl),tt:null,v:null,vt:t.net_val,vs:t.svcnet_val,vat:t.vat,vv:t.vat_val,vsv:t.svcvat_val,det:!1}:(n={invrqid:t.InvRqId,id:t.Id||"",typ:t.Type||"other",p:"",q:null,t:"",tt:null,v:null,vt:t.net_val,vs:t.svcnet_val,vat:t.vat,vv:t.vat_val,vsv:t.svcvat_val,det:""!==(t.Note||"")&&!1===a,SetItmId:t.SetItmId||null},$$.dc("ibtn ico move",d,{title:$rct.mR}),n.p=t.position||t.SortOrder||"",""===n.id?n.t="":["Text","Title"].includes(n.typ)&&0===(t.net_val||0)?n.t=t.htmltext||("#"!==(t.NameOrNumber||"").substr(0,1)?r($$[0]("p").text(t.NameOrNumber)):"")+(t.Note||""):(n.tt=n.det?"":$$.s(t.Note||"").text(),n.q=t.quantity||(0!==(t.quantityhours||0)?fnum(t.quantityhours)+" "+(t.UnitString||""):""),n.t=t.htmltext||(n.det?r($$.s(t.NameOrNumber||""))+r($$.dc("desc").html(t.Note)):r($$.s(t.NameOrNumber||""))),n.v=t.net,n.vt=t.net_val)),""!==(t.Note||"")&&$$.dc("ibtn add",d).append(gi("object-align-left")).click((function(t){$inv.rrw.call(e.tC("hidenote"))}));let s=[$$.tdc("aux").append(d),$$.tdc("keep").text(n.p)];if(""===n.id)s.push($$.td(e,{colspan:4}).append(n.t));else{Array.prototype.push.apply(s,n.q?[$$.tdc("keep").text(n.q)]:[]);let e=e=>null==e?"":fnum(e,$rct.cst);Array.prototype.push.apply(s,[$$.tdc("txt",{colspan:n.q?1:2,title:n.tt}).append(n.t),$$.tdc("currency").text(e(n.v)),$$.tdc("currency inetval").text(e(n.vt)).attr("title",$rct.svcPart+": "+e(n.vs))])}e.empty().attr("class",i?"placeholder":"itm").aC(n.Typ).tC("sethdr","set"===(n.typ||"").toString().toLowerCase()).tC("hidenote",a).append(s),t.co=n},$inv.toSetPrice=function(e){let t=(e.data.data().Id||"").toString();""!==t&&!1!==confirm($rct.toSetPc)&&$.when($inv.t_fds_inv()).done((()=>{$inv.d.sync({Target:"item.setprice",Ref:t})}))},$inv.invSumUpdate=function(){let e=$(this),t=[],n=e.children("tbody");n.each(((e,n)=>{let i=$(n),a=i.data()||{},r=[],l=[],d=i.find("tr.itm"),s=0;i.tC("empty",d.length<1),d.each(((e,t)=>{let n=$(t).data()||{};r.push(n.co);let i=$inv.itemToContract(n),a=(n.SetItmId||"").toString();"set"!==i.type&&""!==a&&a!==i.id&&(i.setId=a),l.push(i),(void 0===n.SortOrder||null===n.SortOrder?-1:n.SortOrder)>-1&&(!1===["text","title"].includes((n.Type||"other").toLowerCase())&&s++,n.SortOrder=0,n.position=s,$inv.rrw.call(t))})),t.push({Id:a.Id,nme:a.Name,text:a.text,itm:r,items:l})}));let i=e.find("tbody:not(.empty)").length;n.find("tr.isum").tC("hidden",i<2),e.data("bai",t),""===(e.data("dtoken")||"")&&!1===bool(e.data("dseeding"),!1)&&null!=(e.data("admin")||{}).type&&(e.data("dseeding",!0),$inv.d.seed($.extend($inv.invcPayload(e.data()),{invid:e.data("invid")||""})))},$inv.worknotes=function(e){let t="";return e.steps.forEach(((e,n)=>{let i;try{i=JSON.parse(e.Data||{}).fields||[]}catch(e){console.debug(e),i=[]}!0!==Array.isArray(i||"")&&(i="object"==typeof i&&!0===Array.isArray(i.field||"")?i.field:[]),i.forEach(((e,n)=>{"Ausgeführte Arbeiten"===e.name&&(t=e.result||"")}))})),t},$inv.rendersrq=function(){let e=$(this).empty(),t=e.is(".onesum"),n=e.data(),i=$$.tr(e,{id:"srq"+n.Id}).aC("title nosort"),a=($rcol.itm.lbl(),$$.dc("axf").appendTo($$.tdc("aux",i)));$$.dc("ibtn osum",a,{title:$rct.combP}).append(gi("euro")).click((function(t){e.tC("onesum"),$inv.rendersrq.call(e),$inv.t_fds_inv()})),$$.dc("ibtn setvat",a,{title:$rct.sV}).append(gi("gbp")).click(i,$inv.setVat),$$.dc("ibtn insb",a,{title:$rct.iRb}).append(gi("indent-left")).click(i,$inv.inRow);let r,l=$$.sc("text",n.text),d=($$.td(i,{colspan:t?4:5}).append(l),["net_val","vat_val","svcnet_val","svcvat_val","net"]);if($$.dc("ibtn edit",a).data("dialog",$rcol.req.lbl().Name).append(gi("pencil")).click({t:l,change:e=>{n.text=e,$inv.t_fds_inv()}},$inv.eHtml),t&&($$.tdc("currency isumval",i),r={Id:n.Id.toString()+"_osum",net_val:0,vat_val:0,svcnet_val:0,svcvat_val:0,net:0},r.tbl=$$.tblset({class:"stbl"})),$.each(n.items||[],((n,i)=>{let a,l={Id:i.Id,net_val:i.net_val||0,vat_val:i.vat_val||0,svcnet_val:0,svcvat_val:0,net:i.net||0,Note:i.Note||""};if("service"===i.Type.toLowerCase())l.svcnet_val=i.net_val||0,l.svcvat_val=i.vat_val||0;t?(a=$$.tr(r.tbl.bdy,{id:"itm"+i.Id,class:"sitm"}).aC(i.Type),"Text"===i.Type||"Title"===i.Type?$$.td(a,{colspan:2}).html(i.htmltext||i.Note):($$.tdc("keep",a).text(i.quantity||((i.quantityhours||0)>0?fnum(i.quantityhours)+(i.UnitString||"").eine(" ",""):"")),i.htmltext?$$.tdc("txt",a).html(i.htmltext):$$.tdc("txt",a).text(i.NameOrNumber).attr("title",i.Note)),$.each(d,((e,t)=>{r[t]+=l[t]})),a.data(l)):($.extend(l,i),a=$$.tr(e,{id:"itm"+i.Id,class:"itm"}),a.data(l),$inv.rrw.call(a))})),t){let t=$$.tr(e,{id:"itmsq"+n.Id,class:"itm osum"}).data(r);$inv.rrw.call(t)}else{let t=$$.tr(e).aC("isum nosort");$$.tdc("aux",t),$$.td(t,{colspan:4}).text($rct.iSum),$$.tdc("currency isumval",t)}},$inv.t_fds_inv=()=>{let e=$("div.invoice_layout table.invi");if(e.trigger("fds.inv"),""!==(e.data("dtoken")||""))return $inv.d.syncChanged(e);let t=e.data("dseedpromise");return t?$.when(t).then((()=>$inv.d.syncChanged(e))):$.when()},$inv.sedit=()=>{$inv.sprev(!0)},$inv.jdisp=function(e){e.stopPropagation(),e.data.id&&$inv.disp(e.data.id,e.data.typ||"")},$inv.disp=(e,t)=>{let n="";switch(t){case"inv":n="inv/rdoc";break;case"rem":n="rem/rdoc"}""!==n&&$ocms.postXT({url:$ocms.url(n),data:{id:e||"",typ:"img"},success:e=>{let t=$$.dc("imagecollection pdfpreview"),n=Math.round(.88*vh());e.id;$.each(e.img||[],(function(e,i){$$.dc("pdfp",t).append($$.img(i).css("max-height",(n-rpx(6)).toString()+"px"))})),$ocms.dlg(t,{size:[n,Math.round(.88*vw())],zindex_min:50,form:!1,exclusive:!1})}})},$inv.jdbn=function(e){$ocms.postXT({url:$ocms.url("inv/rdocn"),data:{name:e.data.id||"",typ:"img"},success:e=>{let t=$$.dc("imagecollection pdfpreview"),n=Math.round(.88*vh());e.id;$.each(e.img||[],(function(e,i){$$.dc("pdfp",t).append($$.img(i).css("max-height",(n-rpx(6)).toString()+"px"))})),$ocms.dlg(t,{size:[n,Math.round(.88*vw())],zindex:50,form:!1})}})},$inv.sp13b=()=>{var e=$("div.invoice_layout").find("table.invi"),t=e.data();t.admin.p13b=!0,!1===(t.inv.InvoiceOptions||"").split(",").includes("§13b")&&(t.inv.InvoiceOptions+=",§13b"),e.trigger("fds.inv"),$inv.d.sync({Target:"p13b",Value:t.admin.p13b})},$inv.itemToContract=function(e){let t=((e=e||{}).Type||"").toString().toLowerCase(),n=e=>e??null,i={id:(e.Id||"").toString(),type:t,title:"",desc:"",qty:"",price_net:"",total_net:n(e.net_val),vat:e.vat||""};var a;return e.co&&"osum"===e.co.typ?(i.desc=e.co.t||"",i.total_net=e.net_val||0):["text","title"].includes(t)&&0===(e.net_val||0)?(i.desc=e.htmltext||("#"!==(e.NameOrNumber||"").substr(0,1)?(a=$$[0]("p").text(e.NameOrNumber||""),$$.d().append(a).html()):"")+(e.Note||""),i.total_net=""):(e.htmltext?i.desc=e.htmltext:(i.title=e.NameOrNumber||"",i.desc=e.Note||""),i.qty=e.quantity||(0!==(e.quantityhours||0)?fnum(e.quantityhours)+(e.UnitString?" "+e.UnitString:""):""),i.price_net=n(e.net),i.total_net=n(e.net_val)),i},$inv.ssetmode=()=>{let e=$("div.invoice_layout").find("table.invi").data();e.admin=e.admin||{};let t,n=e.admin.setmode||"setprice",i=e=>$$.dc("btn",$ict.setmo[e]).tC("selected",n===e).click((()=>{t.c.trigger("modal_close"),$inv.setSetmode(e)})),a=$$.dc("choicefrm").append([i("setprice"),i("setonly")]);t=$ocms.dlg(a,{width:800})},$inv.setSetmode=e=>{if(!1===confirm($rct.toSetMc||$rct.toSetPc))return;let t="setonly"===e?"block.setonly":"block.setprice";$.when($inv.t_fds_inv()).done((()=>{$inv.d.sync({Target:t})}))},$inv.sctp=()=>{let e=$invcol.ctp;$ocms.dlgform(e,{title:$ict.ctp,success:function(e){var t=$("div.invoice_layout"),n=t.find("table.invi").data();let i={};void 0!==n.new&&"{"===(n.new.CustomValues||"").substr(0,1)&&(i=JSON.parse(n.inv.CustomValues)),i.contactName=e.name,i.contactEmail=e.email,n.new.CustomValues=JSON.stringify(i),t.find(".ctpfrm").text(ne(e.name,e.email)),$inv.d.sync({Target:"contact",Value:{name:e.name,email:e.email}})},typedvalues:!0})},$inv.invcPayload=function(e){e=e||{};let t=$.extend({},e.new),n=$.extend({},e.admin);return t.title=null!=t.invoicetitle?t.invoicetitle:t.title||"",t.provisionlocation=null!=t.loc?t.loc:t.provisionlocation||"",t.paymentterm=null!=n.paymentterms?n.paymentterms:t.paymentterm||"",n.customerid=null!=n.customerid?n.customerid:n.CustomerId,{admin:n,req:e.bai,new:t}},$inv.ssave=()=>{$inv.d.save()},$inv.sprev=e=>{$inv.d.preview()},$inv.rReload=()=>{try{let e=$("#listframe ul.rql:first").data();$inv.cInv2({id:e.search})}catch(e){}},$inv.quantChange=function(e){},$inv.storno=function(e,t){let n,i=$$.dc("choicefrm").append([$$.dc("btn","Storno ohne Details").click({id:e,mode:"simple"},(e=>{n.c.trigger("modal_close"),$inv.cSt(e.data)})),$$.dc("btn","Storno mit neuer Rechnung").click({id:e},(e=>{n.c.trigger("modal_close"),$inv.ccStInv(e)})),$$.dc("btn","Storno mit best. Rechnung").tC("inactive",!1===bool(t,!1)).click({id:e,mode:"copy"},(e=>{!0===bool(t,!1)&&(n.c.trigger("modal_close"),$inv.cSt(e.data))}))]);n=$ocms.dlg(i,{width:1e3})},$inv.credit=function(e,t){let n,i=$$.dc("choicefrm").append([$$.dc("btn","Gutschrift").click({id:e,mode:"credit"},(e=>{n.c.trigger("modal_close"),$inv.cSt(e.data)}))]);n=$ocms.dlg(i,{width:1e3})},$inv.setPyd=function(e){confirm($ict.cpyd)&&$ocms.postXT({url:$ocms.url("inv/setpyd"),timeout:60,data:{id:e},success:e=>{alert($ict.relm)},error:()=>{alert($t.f1)}})},$inv.setUpd=function(e){confirm($ict.cupd)&&$ocms.postXT({url:$ocms.url("inv/setupd"),timeout:60,data:{id:e},success:e=>{alert($ict.relm)},error:()=>{alert($t.f1)}})},$inv.resendRem=function(e){e.stopPropagation(),e.data.id&&confirm(string($ict.remresc,[e.data.name]))&&$ocms.postXT({url:$ocms.url("rem/resend"),timeout:60,data:{id:e.data.id},success:t=>{alert(string($ict.remresr,[e.data.name]))},error:()=>{alert($t.f1)}})},$inv.dspRem=function(e){let t=$$.dc("rfrm").ldng(1),n=$ocms.dlg(t,{width:1e3});n.ft.rwText($rct.rq2),$ocms.postXT({url:$ocms.url("inv/getrem"),timeout:60,data:{id:e,drafts:!1},success:e=>{n.ft.empty();let i=$$.tblset({class:"invtbl"},t.empty()),a=$invcol.rem2,r=$$.tr(i.hd);$$.th(r);$.each(a.fields||[],((e,t)=>{$$.th(r).text(t.label)}));let l=!1;$.each(e,((e,t)=>{l=!l;let n=$$.tr(i.bdy).tC("alt",l),r=$$.td(n);n.click((function(){n.tC("selected").siblings().rC("selected")})),!0===bool(t.hasFile,!1)&&($$.dc("idl ilbtn",r,{title:$ict.dl+"\n"+t.DocumentName}).append(gi("save-file","ico")).click({id:t.Id},$inv.downloadrem),$$.dc("idl ilbtn",r,{title:$ict.remdsp+"\n"+t.DocumentName}).append(gi("eye-open","ico")).click({id:t.Id,typ:"rem"},$inv.jdisp),$$.dc("idl ilbtn",r,{title:$ict.remres+"\n"+t.DocumentName}).append(gi("refresh","ico")).click({id:t.Id,typ:"rem",name:t.DocumentName},$inv.resendRem)),$.each(a.fields||[],((e,i)=>{let a=$$.td(n).aC(i.dtype),r=t[i.name];if("function"==typeof i.dfnc)i.dfnc.call(a,r,t);else switch(i.type||""){case"date":a.text(fdt(t[i.name],"dd.MM.yy"));break;case"datetime":a.text(fdt(t[i.name]));break;case"html":a.append($$.dc("ctw").html(r)),a.append($$.dc("ttip").html(r));break;default:a.text(t[i.name])}if("InvoiceId"===(i.name||""))a.aC("keep");switch(typeof i.title){case"function":i.title.call(a,t);break;case"string":a.attr("title",cs.title)}}))}))},error:()=>{t.empty(),n.ft.rwText($t.f1)},complete:()=>{t.ldng(0)}})},$inv.ccRem=function(e,t){$(this);$ocms.postXT({url:$ocms.url("rem/lrem"),timeout:60,data:{id:e},success:n=>{let i=$invcol.rid.clone();i.applyValues(n.ov);let a=$$.dc("ac"),r=$$.tblset({class:"fullgrid fullwidth"},a);if((n.lst||[]).length>0){$$.d({style:"margin: 1.5rem 0 1rem 0;font-size: 110%;text-decoration: underline;"}).prependTo(a).text($ict.rovlh);let e=$$.tr(r.hd);$ict.rovl.forEach(((t,n)=>$$.th(e,t))),$.each(n.lst,((e,t)=>{$$.tr(r.bdy).append([$$.tdc("keep",t.subject),$$.tdc("currency",fnum(t.amount,$rct.cst)),$$.tdc("currency",fnum(t.amount_payed,$rct.cst)),$$.tdc("keep",fdt(t.DateFinalized,"dd.MM.yy"))])}))}else $$.td($$.tr(r.bdy),$ict.nd);$ocms.dlgform(i,{addcontent:a,title:string($ict.remdt,[t||"?"]),success:function(t){$inv.ccRem_s2(e,t)},typedvalues:!0})}})},$inv.rRemRw=function(e){let t=$(this),n=e.rm||{};t.empty().data({invoiceid:n.invoiceid,invoicedate:n.invoicedate,amount:n.amount,amount_payed:n.amount_payed});let i=$$.dc("axf").append($$.dc("ibtn edit",{title:$rct.cP}).append(gi("pencil")).click(t,$inv.eRowR));t.append([$$.tdc("aux").append(i),$$.tdc("keep",n.invoiceid),$$.tdc("keep",fdt(n.invoicedate,"dd.MM.yy")),$$.tdc("currency",fnum(n.amount,$rct.cst)),$$.tdc("currency",fnum(n.amount_payed,$rct.cst)),$$.tdc("currency",fnum(n.amount-n.amount_payed,$rct.cst))])},$inv.eRowR=function(e){let t=$(this),n=e.data,i=n.data()||{},a=$invcol.rem.clone().applyValues(i);$ocms.dlgform(a,{title:t.data("dialog")||"",success:function(e){let i=t.closest("table"),a=i.data();$.extend(a.rm,e),i.data(a),$inv.rRemRw.call(n,a),$inv.rd.syncAmount(a.rm.amount,a.rm.amount_payed)},typedvalues:!0})},$inv.ccRem_s2=function(e,t){$fis.lf(!1).rC("fix").aC("hd");let n=$$.dc("invoice_layout",$fis.frm_edit()).append($$.dc("btn sprev").click($inv.rprev));$inv.eM(!1,!0);$$.dc("rfrm").ldng(1);$ocms.postXT({url:$ocms.url("rem/get"),timeout:60,data:$.extend({id:e},t),success:e=>{let t=e.rm||{},i=$$.dc("srq",n),a=$$.tblset({class:"invi"},i);a.ft=$$[0]("tfoot"),a.tbl.data($.extend({invid:t.invid,new:{}},e));let r=$$.tr(a.hd).aC("shd").append([$$.th().aC("aux")]);$ict.remHR.forEach((e=>$$.th(r,e))),$inv.rRemRw.call($$.tr(a.bdy),a.tbl.data()),a.ft.appendTo(a.tbl);let l=(e,t,i,r,l)=>{let d=$$.dc("inpfrm",n).aC(e).append("string"==typeof r?$$.dc("ahd",r):r>0?$$.dc("ahd",$rct.frm[i]):null),s=$$.dc("content",d).rwText(t);$$.dc("axf",d).append($$.dc("ibtn edit").data("dialog",$rct.frm[i]).append(gi("pencil")).click($.extend({t:s,nme:i,change:e=>{a.tbl.data("new")[i]=e}},l),$inv.eHtml)),a.tbl.data("new")[i]=t};l("tfrm",t.subject,"subject",0,null),l("adrfrm",t.invoiceaddress,"invoiceaddress",0,null),l("emailfrm",t.invoiceemail,"invoiceemail",0,null),$$.dc("sndfrm",n).append($$.dc("content").text(t.sender)),a.tbl.children("tbody").each($inv.bdysort),a.tbl.trigger("fds.inv");let d=a.tbl.data("new");d.amount=t.amount,d.amount_payed=t.amount_payed,$inv.rd.seed({rem:{invid:t.invid,type:t.type,invoiceid:t.invoiceid,invoicedate:t.invoicedate},new:d})},complete:()=>{}})},$inv.rprev=()=>{$inv.rd.preview()},$inv.sis=e=>{confirm($ict.sisc)&&$ocms.postXT({url:$ocms.url("inv/sis"),data:{id:e||""},success:e=>{}})},$inv.srs=e=>{confirm($ict.srsc)&&$ocms.postXT({url:$ocms.url("rem/srs"),data:{id:e||""},success:e=>{}})},$inv.mfrrel=e=>{$("#contentframe").ldng(),$ocms.postXT({url:$ocms.url("inv/mfrrel"),data:{id:e||""},success:e=>{$inv.rerenderinv()},complete:()=>{$("#contentframe").ldng(0)}})}; \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.inv.min.css b/Fuchs/wwwroot/web/fis.inv.min.css index 5964b5a..dbcbcb5 100644 --- a/Fuchs/wwwroot/web/fis.inv.min.css +++ b/Fuchs/wwwroot/web/fis.inv.min.css @@ -1 +1 @@ -.invfrm>table td.sh_s,table.invtbl td.sh_s{color:#bf4b06}.invfrm>table td.is_cc,table.invtbl td.is_cc{background-color:#777!important;color:#fff!important}.invfrm>table td.is_pyd,table.invtbl td.is_pyd{color:#60cc08}.invfrm>table td.is_due,table.invtbl td.is_due{background-color:#f2f23f!important}.invfrm>table td.is_ovd,table.invtbl td.is_ovd{background-color:#de4e09!important}.invfrm>table td.is_rem,table.invtbl td.is_rem{background-color:#ce1900!important}.invfrm>table td.av,table.invtbl td.av{border:2px solid red}.invfrm>table td.av+td.vsel select,table.invtbl td.av+td.vsel select{display:block}.rfrm{min-width:200px;min-height:200px}table.if{border-collapse:collapse;margin:1rem 0;background:#fff}table.if tr:nth-child(2n+1) td{background-color:#f9f9f9}table.if tr.title td{background-color:#56a532;color:#fff}table.if tr.title td .eid{font-weight:700;margin:0 1rem 0 .5rem;font-size:140%}table.if tr.title td .nme{font-size:110%}table.if tr.shd td{font-style:italic;color:#bbb;background-color:#fff!important;font-size:90%}table.if td,table.if th{padding:.2rem .35rem;border:1px solid #ddd}table.if td.currency{text-align:right;white-space:nowrap}table.if td.num{white-space:nowrap}.pdfpreview .pdfp.ph>.note{background:#fff;display:inline-block;padding:3rem;margin:.2rem 0}@keyframes fis_spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.edit_frm .invoice_layout{margin:2rem 2rem 2rem auto;width:50rem;border:1px solid #eee;padding:25.5rem 3rem 3rem;box-shadow:4px 4px 9px rgba(50,50,50,.3);position:relative}.edit_frm .invoice_layout .axf{font-size:1rem;display:none;position:absolute;right:100%;height:100%;min-height:1.5rem;min-width:2rem;background:#f0ffe2;border:1px solid #ccc;border-top-left-radius:.2rem;border-bottom-left-radius:.2rem;top:0;width:auto;max-height:1.5rem;overflow:hidden;padding:.1rem}.edit_frm .invoice_layout .axf>.ibtn{cursor:pointer;height:100%;width:1.5rem;display:inline-block;position:relative}.edit_frm .invoice_layout .sndfrm{position:absolute;font-size:.7rem;color:#727272;top:4.7rem;left:3rem;width:auto;height:auto;user-select:none}.edit_frm .invoice_layout .emailfrm{position:absolute;font-size:.7rem;top:.5rem;left:.5rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .emailfrm:hover div.axf{display:block}.edit_frm .invoice_layout .admfrm{position:absolute;right:3rem;top:9rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .admfrm .ahd{display:block;color:#727272;margin:0 0 .2rem}.edit_frm .invoice_layout .admfrm .content{color:#000}.edit_frm .invoice_layout .admfrm .content:empty::before{content:"-";min-width:5rem;height:1rem;display:block;position:absolute}.edit_frm .invoice_layout .admfrm:hover div.axf{display:block}.edit_frm .invoice_layout .ctpfrm{position:absolute;right:3rem;top:14rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .ctpfrm .ahd{display:block;color:#727272;margin:0 0 .2rem}.edit_frm .invoice_layout .ctpfrm .content{color:#000}.edit_frm .invoice_layout .ctpfrm .content:empty::before{content:"-";min-width:5rem;height:1rem;display:block;position:absolute}.edit_frm .invoice_layout .ctpfrm:hover div.axf{display:block}.edit_frm .invoice_layout .adrfrm{position:absolute;left:3rem;top:6rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .adrfrm:hover div.axf{display:block}.edit_frm .invoice_layout .locfrm{position:absolute;left:3rem;top:18.5rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .locfrm .ahd{display:block;color:#727272;margin:0 0 .2rem}.edit_frm .invoice_layout .locfrm .content:empty::before{content:"-";min-width:5rem;height:1rem;display:block;position:absolute}.edit_frm .invoice_layout .locfrm:hover div.axf{display:block}.edit_frm .invoice_layout .tfrm,.edit_frm .invoice_layout>div.title{position:absolute;left:3rem;top:15rem;font-size:2rem;cursor:pointer}.edit_frm .invoice_layout .tfrm:hover div.axf,.edit_frm .invoice_layout>div.title:hover div.axf{display:block}.edit_frm .invoice_layout div.fnote{margin:1rem 0}.edit_frm .invoice_layout p+table.invi,.edit_frm .invoice_layout table.invi+p{margin-top:2.5rem}.edit_frm .invoice_layout table.invi{border-collapse:separate;border-spacing:0;border-color:#fff;width:100%;max-width:100%}.edit_frm .invoice_layout table.invi>tbody>tr.hidden{display:none}.edit_frm .invoice_layout table.invi>tbody>tr:nth-child(2n+1) td{background-color:#fbfbfb;border-color:#fbfbfb}.edit_frm .invoice_layout table.invi>tbody>tr.title>td{background:#fff;border-color:#fff;border-top:1.5px solid #000}.edit_frm .invoice_layout table.invi td.currency,.edit_frm .invoice_layout table.invi td.num,.edit_frm .invoice_layout table.invi th.currency,.edit_frm .invoice_layout table.invi th.num{text-align:right;white-space:nowrap}.edit_frm .invoice_layout table.invi td.keep,.edit_frm .invoice_layout table.invi th.keep{white-space:nowrap}.edit_frm .invoice_layout table.invi table.stbl td,.edit_frm .invoice_layout table.invi table.stbl th{padding:.3rem .5rem}.edit_frm .invoice_layout table.invi table.stbl td:first-child,.edit_frm .invoice_layout table.invi table.stbl th:first-child{padding-left:0}.edit_frm .invoice_layout table.invi table.stbl td:last-child,.edit_frm .invoice_layout table.invi table.stbl th:last-child{padding-right:0}.edit_frm .invoice_layout table.invi>tbody>tr>td,.edit_frm .invoice_layout table.invi>tbody>tr>th,.edit_frm .invoice_layout table.invi>tfoot>tr>td,.edit_frm .invoice_layout table.invi>tfoot>tr>th,.edit_frm .invoice_layout table.invi>thead>tr>td,.edit_frm .invoice_layout table.invi>thead>tr>th,.edit_frm .invoice_layout table.invi>tr>td,.edit_frm .invoice_layout table.invi>tr>th{border-width:1px;padding:.3rem .5rem;border-left:none!important;border-right:none!important}.edit_frm .invoice_layout table.invi>tbody>tr>td.aux,.edit_frm .invoice_layout table.invi>tbody>tr>th.aux,.edit_frm .invoice_layout table.invi>tfoot>tr>td.aux,.edit_frm .invoice_layout table.invi>tfoot>tr>th.aux,.edit_frm .invoice_layout table.invi>thead>tr>td.aux,.edit_frm .invoice_layout table.invi>thead>tr>th.aux,.edit_frm .invoice_layout table.invi>tr>td.aux,.edit_frm .invoice_layout table.invi>tr>th.aux{width:0;max-width:0;min-width:0;position:relative;padding:0}.edit_frm .invoice_layout table.invi>tbody>tr>th,.edit_frm .invoice_layout table.invi>tfoot>tr>th,.edit_frm .invoice_layout table.invi>thead>tr>th,.edit_frm .invoice_layout table.invi>tr>th{text-align:left;font-weight:300;background-color:#fff;border-color:#fff;font-size:.8rem;font-style:italic;border-top-style:none;border-bottom-style:none}.edit_frm .invoice_layout table.invi>tbody>tr>td,.edit_frm .invoice_layout table.invi>tfoot>tr>td,.edit_frm .invoice_layout table.invi>thead>tr>td,.edit_frm .invoice_layout table.invi>tr>td{vertical-align:top;font-weight:400;background-color:#f8f8f8;border-color:#f8f8f8;border-top-style:solid;border-bottom-style:solid;user-select:none}.edit_frm .invoice_layout table.invi>tbody>tr:hover>td.aux>.axf,.edit_frm .invoice_layout table.invi>tfoot>tr:hover>td.aux>.axf,.edit_frm .invoice_layout table.invi>thead>tr:hover>td.aux>.axf,.edit_frm .invoice_layout table.invi>tr:hover>td.aux>.axf{display:inline-flex}.edit_frm .invoice_layout table.invi>tbody>tr.isum>td,.edit_frm .invoice_layout table.invi>tfoot>tr.isum>td,.edit_frm .invoice_layout table.invi>thead>tr.isum>td,.edit_frm .invoice_layout table.invi>tr.isum>td{border-top:1px solid #000;background:#fff}.edit_frm .invoice_layout table.invi>tbody>tr.sep>td,.edit_frm .invoice_layout table.invi>tfoot>tr.sep>td,.edit_frm .invoice_layout table.invi>thead>tr.sep>td,.edit_frm .invoice_layout table.invi>tr.sep>td{background:#fff}.edit_frm .invoice_layout table.invi>tbody>tr.tsum td,.edit_frm .invoice_layout table.invi>tfoot>tr.tsum td,.edit_frm .invoice_layout table.invi>thead>tr.tsum td,.edit_frm .invoice_layout table.invi>tr.tsum td{font-weight:600;border-top:1px solid #000;border-bottom:1px solid #000}.edit_frm .invoice_layout table.invi .sortable>tr>td.aux .ico.move{background:url(data:image/svg+xml;base64,PHN2ZyBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAzMiAzMiIgaWQ9IkxheWVyXzQiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48Zz48cG9seWdvbiBmaWxsPSJub25lIiBwb2ludHM9IjE2LDMxIDgsMTggMjQsMTggICAgICIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iI0RERERERCIvPjxwb2x5Z29uIGZpbGw9Im5vbmUiIHBvaW50cz0iMTYsMSA4LDE0IDI0LDE0ICAgICAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiNEREREREQiLz48L2c+PC9zdmc+) center center/contain no-repeat border-box}.edit_frm .invoice_layout table.invi .sortable>tr>td.aux .ico.move:hover{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzIgMzIiIGlkPSJMYXllcl80IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGc+PHBvbHlnb24gZmlsbD0ibm9uZSIgcG9pbnRzPSIxNiwzMSA4LDE4IDI0LDE4ICAgICAiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2Utd2lkdGg9IjIiLz48cG9seWdvbiBmaWxsPSJub25lIiBwb2ludHM9IjE2LDEgOCwxNCAyNCwxNCAgICAgIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9nPjwvc3ZnPg==)}.edit_frm .invoice_layout table.invi .sortable>tr.sortactive>td.ico{cursor:move}.edit_frm .invoice_layout .btn{background:url("") center center/contain no-repeat content-box #fff;position:absolute;top:.2rem;right:.2rem;height:3rem;width:3rem;padding:.1rem;z-index:11}.edit_frm .invoice_layout .btn.sprev{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAaz0lEQVR4nO2caVAbd97neSbObPJs7VE7z4vU82Krdp+t2qmt3apnNhObIzanwfjKZBJPJpXJU45rbGcSH4mvCZM4g08yPjC2s/gCJjG2sfEB2BhkA+ISoPtAal3dUnerW/fRkgAnYbLz3RfQHQHCHHby7Ivpqk9JdLcE+vh//H6//1/OyPjb8bfj/6sDwI8IgnjBarX+T4fD8dKpU6fyPv744w379+//zYEDB0pVKlUWSZI/M5vN/8Rx3PP/2n/vEx2NjY3PbNmy5dny8vIls9HY2PjMnj17/u1HH330X48dO/bazp07q3ft2jV05MgR75kzZ5I1NTXfNDQ0/OXmzZvf1tfXj1+4cOHRsWPHIp988ol9+/btt0+cOLH74MGDL77//vs/aWxsfKa8vPxHjY2Nz8yH8vLyH80HAH83F4sStGHDhv+QkZHxv5977rmCFPJElixZkveTn/zkX4qKii7v2LGDOnfu3JhMJvvr8PAwSJIETdNgWRYcx8HL8+A4DgzDgKIo2O12qFQq3Lhx4y+HDx8Ov/HGG50//elPy5577rmC559/PnvJkiXzISuVZ599NvPZZ59dloalc/DSCy+88D8UCsW/W5CgjRs3/seXXnrps9raWu+1a9f4a9eu8Q0NDXx9fT1/5MgRfvPmzd7y8vJke3v7XwmCAMuy8PI8PCwLN02DpChYHU6YrHYYrXZYbA44nE5QLhcYhgHn8YDnOFAUBbVajbq6um/ef//9wIcffshdunTJc+3aNe7q1avctWvXZtDQ0OARuX79uuf69eue1HOp5+fixo0bnhMnTpgbGhoKF9yCiouLz/M8j5GREYyOjsLn8+HGjRs4c+YMWlpaYLPZwHMcPCwLu5OE0mhBm9KAqz1q1HWqcKlThfMdKpx7qMLFDhVqOlX4Uq5C84AOfToTjIQVbrcbnMcDhmGgVCpRV1eH6upq6PV66feOjIzMyujo6JyMjY3NyqNHj6BUKhPt7e3rFyxo5cqV5zweD5LJJBwOB86fP4+LFy/CaDRKXcZIWNGmNOBytwb/54ESle1KnBRpG5ok9fkQKtuVOC1ToqZLjdsKHYYMZjicTng8LCiKQltbGyorK9HR0YFYLIZEIoF4PI5EIjHjeTKZTEvqtbkEDwwMLE5QUVFRtcfjgU6nw6lTp3Dz5k2QJAmWZWGx2fFQbcSfuzU481CN0w/VOJvCmVlId8/5Tg2aBvRQmyygXC6wLIuBgQFUVVWhsbERwWBQEpMqZzZB85EjXlu0oLfeeuvfFxcXV3d2duLYsWNoa2sDTdOg3W6ojGY09OlQ3alBdZcWF7t1qOnRozaFuh496npTmHZdpKZHjwvyifeq69aiQ22EzWYHwzDQ6/U4c+YM6uvrEQ6H5916Unlc6xkZGcHY2NjiBS1duvTc4cOHIZPJpBmoTzeMy706nOvSobbXgKsDw7g2aEHDkAWNSgJ31Fbc1dpwX2+HzODAA6MDMoMD7QYHWnV2NGtsuK22olFJoGHIgoZBC64NmnFZYcLFbj0uyXVoVRpgJqxgGAYGgwEnTpxAU1OTJGexYsTzqa1n0YLefvvt/1JaWtpz69Yt0DQNJ0miS2vCF716XOox4OqgGXc0NjRr7WjVO9BpptBvdWPQTkNpp6F0MLMyaKfRb3Wj2+KCzOhEi86BJq0dt9RWfNlvQk2PHk2DBhgtBGiaxtDQED777DPcv38fLpcLFEU9FpfLNYNAIDBFzhMJArBk06ZNn50/f/5bp9MJl8s10XL6DKjrM+KW2op2E4WHwy7022joKA+MLk7C4OKgpzhoSA+UDhYDdgb9Nhr9NhoDdgZDDgZqJwsd5YHexUHpZNFhdkFmonDf6MTVQTPqeg1oVRphnpT08OFD7N+/H83NzdBoNFCr1fNGo9GApum0s9+iBNXU1OTs37/fZzabpTHn+oAJX/SbcEdjg5yg0WdnoHfxIFgfrJ4JCNYHE+2FhuIw6PCgz8agZ5Ju6wTiz702Bv12FirSAy3FQWFn0W2l0WNj0GF24YaSwNWBYXRohmGz20GSJGpra1FdXQ2/349IJJKWaDSalng8/nQE0TT93AcffHD53r17oGkaJosVzcph1CuGcUdjQ5+dhYriQHj8IL1BUN4gSG8AVo8fetoLJcVh0PmEkBy6CDduqgg0Dpmh0JtBkiRUKhUOHDiApqYmGI1G6PX6eWEwGMCybNrYacGC6uvr1xw6dEiw2WxwOp2Q68xoGDLjhpKYaDWMDw5vEGwgDDYQhtsfAsEFoKd90Lq9TxWZiUSjisA9tRkmswUkSaKhoQFHjhyByWSCw+GYN36/f4qYRY1BPp/v73fu3NnU3t4OmqahNxNoURNoVBF4MEzByAZA+sLgQ1Hw4SjoQAQEF4SJDXwvaNw+tOqduKu1oV9vhsPhgF6vx8GDByGXyxEMBuH3++ckEAggFovNGmXPW9DVq1f/+Y9//KPXarXC4XCgW29Bi9aGFp0dGrcXpD8CX0RAICrAE4rB5g2D4EPfK312Fvf1Dsh0VhjNFjgcDtTU1KCiogKDg4MYGhqaF263e0rLWZSgo0eP7rh06dK3brcbJgsBmd6Ge3oHHppdsPsi8EXjCAsJeKMCqEAUDn8Uzu8ZAxtAu9EJmcGBIeOEoLa2Nhw4cEDqOj6f77H4/f4pLWj6YK1QKOYW5PP5/v63v/3t9a6uLrhcLqhNFjw0OdFqcGKI5OEJC4jGkwgKCdAhAa5g7AeBDEQhJ2jIzRT6jDYQhBVGowGHDh1Ca2srSJJ87NjjdDrhdDqlMShdADkvQfX19f9tx44drMFggMPhwMCwDXILBdkwBYIPIRRPIpZMgo/GwYSfgEgc7CRMWIArFAM1KcIVjIEOC99dj0y8RkXx6LG40D3shMlsAUEQOHv2LM6cOTOvGcxgMMDj8cwQJOZtfX19idbW1scLOnbs2PKKioqE3W6HxWpFn9mJbsKNDsINdygGITmKUDwJLppYPLEEmEgcTn8URjYAJcWj3+FBn51Fr51Bv4PFIMlBz/jh8EfBROLgY0mYPEHICRf6rS5ohwnYbDbcvHkTp0+fRiAQmDX2iUajiMViUkVgerohXuvt7Z2XoNc+//zzMRdFwWQhoCAo9Fpp9NpZ8NE4hJFR+IQReIXkgvAJSfiEEXDRBOy+CIYoHnIrjU6LGw8n6Zgk9We5lcYQycPChWDhQ+i10hiy01CZrbDbbWhtbUVZWRkUCgW0Wi00Go2EVqudAcMwU1pOLBaTAst5CTp+/PiOCxcujLvdbhjNBFR2N/psNAZJDt5YApHECPzxhRGIj8AbS8Lpj0Lt8qLHxkyMJ9b50UXQ6LbS6LOz6LczUDsZqC12WK1WyGQyfPTRRzCbzXC73VPyLrfbLTE9F0skEpKccDiMSCSCnp6euQVVVVVV1NbWwu1ywWAmoHVOpAJ9DhYGxg9XMAY+lkAgPoJgYpL4NFLO+4QkqEAUWtqHfodHSi967ezCsLHosTFQOFhoSQ80hBMEYYFcLkdZWRmsViu8Xu8UxNkr9VwkEpkhZ6GCTtfV1YEiSRgsVugpDwacHigcEww4OWjcPli4INyhGPhoAt6Y2IWS8MaS4KMJuEMxWL0h6Bk/Bp0c+h3fvceTMOicSGy1VhLDw8OQy+XYs2cPZDIZVCoVlEollEolVCqVhHhOqVTC5XJNkRMKhRAKhRAOhyGXy+cWdPLkyfJLly79lSRJGC1WmFweqCgOQ+R3DE4+KikeapcXOtoHA+uHgfFDR/ugcXuhpPgp9z4pqsn3U1McTG4OehsJo9EImUyGffv2gSRJhEIhBINB6UMHg8Ep+P1+BIPBGXLE18xL0Geffba1srLyG5J0wmQhMOzyQOf2Qu16PKpJ5rpvMejcPti5ANQuL/RuLyw0D4PNCZPJhLa2NuzZswdqtRoWiwUWy8T0Lz4XMZvNMJlMcLvdM+SIgrq6uuYW9Kc//ekX+/fvHyMIAsMWAmYXCxPjg47+18PJB0H7Q9DSPpgYHwiGh9Fqh8lkQmtrKz755BNotVqYzWaJ4eFhidSM3+VySYJEOYFAAMFgcH6Cjh49mvXee+/F9Xo9LAQBC0WD8PhgYPww/sAYGD8ILoBAOArKF4KB8cPK+mBlvDBaCCiVStTX16OyslKqDYmDrighEAhIAzXP8/D7/TPkiIlsZ2fn3IIUCsV/3rhxo62npwdWqxUWBwkH54fZE8TwD4yFC4IPRRCJxmDjQxj2BEFyfhBuDwxGI/r7+nD48GFUVVVNqS6Kg/PQ0BAGBwcxODiIgYEBDAwMwG63S4JEOQsSBODHb731Vm19fT3sdhvMVhtIzgcbHwLBh2H9ASD4EKx8GEwgglgsBl84Cisfho0PwsX7QZBuqFQq9PX14dNPP4VcLgfHcWBZFgzDwO12g6IokCQpIeZiHo9nhhwxke3o6JhbUEZGRsapU6c2lZWVfW02m2G2EHAyHpC+EBz+yA8GG4wiGoshGouBDkZg90fg8gVB8X6YbXb09/fj9u3bKC8vRygUkoI/QRAQjUal0mtqd0vXclIz/XkLam9v/+8bN26ku7q6YLFYYHNSoL0BUIEoyGDse4UKxsCFJ8QIggB/ZOIcFYiA8QXgZDlotFp0dnbi+PHjqKurW5CY6XJSg8qHDx/OTxCAH7/99tu1Z8+ehdiKXB4OTDACd0j43qBDArwRAdGYAEEQEI4KYMMT15hACIzXD8LugFwuR3NzM3bu3AmlUol4PP5Ecnieh9frxYMHD+YnKCMjI+P48eNZv/nNb7w9PT0wm82wOUl4fAGwKWWIpwkfERCKxhCLCRCEGKIxAd7oxDVPKArOHwTFclCr1ZDJZKiqqsLJkycRCoVmyJke48wlR2RBggA888Ybb5yqqKiASqXCsNkMimbBB8PgownwseRTwRdLIBiLT7aamCQnEJsocfDROPhAEKwvgGELgQcPHuDWrVvYu3cvtFpt2q71OEHp5HAcB57nIZPJ5i8oIyMjo6Oj43+9/vrr5JUrV6DVakEQVjAeD3zhKPzC1Iw9JCQQEuIICAn4heTs2b2QRECYqEhGhDhiwndiYrEYooKAkJCQ7vUGQ+D9AdgcTnR1dqKpqQn79u3D5s2bYTQawfM8WJaVYBhmYu8ATc/I5F2TGyKmy/F4POA4buGCMjIyMt55550dW7ZsGb1//z50Oh1sdjs8Xi/8kRhCiVGEkmMIJUYRiScREwTEBAFRQUBUiCMixBEREojEExOPQhzRyXtiQgxC7Dtik68JJ0YQSo5NVAJCYXj9AZAuN/r6+tDS0oJTp06huLgYmZmZeO+99yCTyTA0NCTFOQqFQqK/v38GFotFEiTKeSJBb7755j/87Gc/ay4rK8Pdu3ehVqthdzjg4XgEIlFEkmOIjjyaIDmKWCKJWFxsGYJUrROmEZuUEhMExOIJxBIj0vuEEyPwB0Pw+vwgXW4oFAq0tLSgrq4Or7/+OnJycpCVlYVly5Zhx44d0Ol0UvyT2mLENfrUWIimaXAcN0UOy7LweDxob29fuKCNGzc+l52dfXrXrl0oKytDU1MTlEolrDYbWJZFMBxBNDGC+NhXUxkdQ3xkFEJyBEIiCSGRSCE5QXJ04r7J1wijjxCOxeEPBuH1+WB3ONDd3Y2mpiZcunQJb775JpYvXy6RlZWFzMxM7N27F0NDQ7BYLFNyMJPJBJPJBKPRCKPRCIPBAKfTOUOOyKIFlZaWVg0NDWHv3r345JNPcPv2bfT392N4eBhuNw2vz4dwNIb46BhGHn2Nka++mcbXE+dFpl1Pjn2FWGIEwVAYPr8fHo6H2WJBR0cH7ty5g3PnzuHXv/41cnNzsWLFiilkZ2dj2bJl2Lp1K2QyGZRKZdoUQ8RisUwRwzAMGIZ5MkElJSVVPM/D4XDg8OHD2LVrF2pra9He3g6VSgWbzQaGYeD3BxCORBBPjmBk7CuMfvUNHn09jkffTGXs63GMPPoaidExRIU4gsEQ/P4AOI6HzW6HQqHA3bt3cfPmTVRWVuK1115Dbm5uWkRJmZmZ2LVrF4xGI1iWTTtIUxQFmqYlQaIcmqbBMMyTCRL3KHIch88//xybNm3CyZMncefOHXR2dkKlUoEgCKmP+/2BiUpdJCKlDNFYDJFoFKFweFKKHzzPg2EYWK1WDA4OorW1Fbdv30ZtbS127tyJ0tJS5OXlzYooKicnB8uWLcO2bdsgk8kwODiYdrAeHh6eIUekra1t0YJOeTweaWdXKBRCc3MzPvzwQ+zduxfV1dVobGzE/fv30dPTA41GAwtBwD65XUX8VxM3fJIkCbvDAYKYKFl0dHSgpaUFt27dwtWrV1FRUYFNmzahsLAQmZmZyMvLQ35+flpSZeXk5GDp0qV499130dPTI41D4hhkNBpht9vThgM0TeP+/fsLF7R9+/Z/U1xcXCkKSt0b6HK5UFdXh61bt2Lnzp2orKxEQ0MDbt26hZaWFrS1taGjowNdXV3o7u5Gd3c35HI5ZDIZ7t27h+bmZty+fRs3b97ExYsX8fHHH+Odd97Bvn37cPfuXZw9exZ5eXnIyspCXl4eCgoKJGYTlpOTg8zMTKnKaLdPrH5YrVYQBAGSJGfIEVdAnrqgZDKJSCQCo9GImpoa/P73v8f777+P3bt3o6KiAlVVVfjiiy/Q0NCAhoYGXL9+HY2Njbhy5Qqqq6tRWVmJAwcO4He/+x3effdd7NmzB3V1dejp6YFSqYRCocCxY8eQn5+PrKws5Ofno7CwcIqo6eTn5+Pll1+WBu579+6ht7cXPT096O7uhtFonCFHFNTa2vpkgqbvLk0kEgiHw4hGoxgbG4PP54NGo8GVK1fw0UcfYdu2bdi9ezf27NmDbdu2SfLEn7dt24bKykrIZDKYzWbwPA+fzycFcWKX/POf/4yioiJkZWWhoKAARUVFKCwsnJWCggJJ0vbt26FSqeB0OqUun27wdrlcTyaI47gpYlIFRSIRqR4jrnuLO8G6urrQ0tKCq1ev4vLly7h16xZkMhkUCgV0Oh1MJtOUovr02MVoNEKr1eL48eNSdxMlzYYoavny5cjMzMTu3bthNBpnBJOpgSRFUd+voNHRUanbJRIJUBQFg8EAo9EIk8k0I3gzGAxSIV2n00mkLhOnLiOrVCqcPn0aBQUFyMrKQmFhIVauXJmWVFmpkvR6PWiaThtlUxSFe/fu/XCCxCx7Nmar28xWnvD5fPB4PKipqZEkFRUVobi4GMXFxY+VJUratWsXdDrdjJKs0+kESZI/rCBxQE9FmJafLUYWx3Gora1FYWGhJKmkpEQSlY6VK1dKkj788ENotVq4XK4pteonErRq1ap5C3pacmZrRWJLEiVlZ2dj5cqVKCkpmRVR1IoVK7Bs2TJ88MEH0Gg0UzZdkSSJ1tbW+N27d9c+FUHibCYWqqYLSlcCTbdgl07C9HpNuuTS7XbjwoULKCgoQHZ2NoqLi7Fq1aq0pMrKzc2VWpJOp5PSDJZlceHChcHr16//04IEbdiw4cfTBaVO9dMFia3FbrdLA2zqbvfZNhWkbrQUE83pyWZqrUehUKC3txdHjx5Fbm6uJKm0tFRiNlmipH379sFmsyEUCuHu3bu2o0ePLl+QnMe1oNRAMZ0glmWl5mu322G322Gz2SRSo1uR1HX01OXjdFO/uKVOp9Ph7NmzKCwsRE5ODkpKSrB69eopoqazatWqKZIaGxvZEydOlCxYTroWFI/HEQ6HEY/HZwhKHW/8fn/assJs5dB0xS2n0zmnZJvNBrPZjKqqKikfW7VqFdasWYPVq1fPipgEZ2VlITc3t6+lpeUfn0TQSY7jkEwmQVEUHjx4AJvNNqWLjYyMTBFksVigUCjmVQbt6+uT6O3tlVIDETGPE3O5dHR0dKCiokLKx0RJsyGKys/Px7Jly74tLS29olarX3hiQWazGXK5HBRFIR6PTxGUOlMFg8G0qwfTB9uFtKy5WpXFYkF1dTUKCgqQk5OD0tJSrF27dgpr1qyRHkVR+fn5WLp06berV6+uX7Ck6YKi0Si8Xi8EQZC6WDgcnrJbNBqNStPxdBmp9ZfpKw6zraen7ncWZx3xvaYLdTgcOHfunCRp9erVWLduHdatWzdDVqo0UVJpaenFBX01XBTE83zab/RNFyTGNxaLZday52zdLl1XS+1y/f39cDgcaeWkfpHO6XSirq4ORUVFkqT169dLotKxdu1a5Ofn48UXX0weOHBgw7wFbdmy5dmSkpIT0wWJM5m4vy+RSEwJ/rxe76zdZ64uJLYasQuldiOXyzWrHPE9KIqC3+/HlStXJElr1qzB+vXrH8vq1auxdOlSHDp06PC8BZWXly9JFZQaCwmCIEW/oqDHrY/Pdxl4trEqXddMN/uJ+xXD4TC+/PJLFBYW4uWXX8batWvxyiuvpGX9+vVYu3Ytli5dOn7o0KHtixY0PXUQ9/elDtjzWf6dTYwoJ90gPp/QQEwbxM2afr8fNTU1/zcvL+/b5cuXY+3atfjFL34h8corr0iPRUVFWLFihaqpqWn+0bQoiOO4tDlVqqD5tJp0ckQxC5UzfTAXBVEUJe07DAQCuHz5cn92dvbhl156KbRixQqsW7cOr7766hRRxcXFyM/PtzQ2NubOW85sglJnK/EPEQThieQ8bvqfjxzxGz2iIHFbXXNzs2nPnj3LADzzq1/96t2f//znkqRf/vKXePXVV7Fy5Urk5eWZFywnnaDp2XiqoPlk43PJmU9cNJsc8bnL5YLf70d7ezv1hz/8YaX4WQAs2bBhw5YXX3zRl52djfz8fKxYsQK5ubmmRclJFcTzvFQIm08XW2zG/rgAcraxZzo0TUMul/Offvrpa9P/XyAAS44cOVJaXFx8tbi4uKukpORsY2PjPy9KzuQb/l1BQcHh7u7uvyiVyvGhoaHxwcFBiYGBgXGtVjuu0WjG+/v7Jfr6+sb7+vrGe3t7x3t7e8d7enokuru7x7u7u8flcvm4XC4f7+rqGu/q6hrv7Owc7+zsHO/o6Bjv6OgYf/jwocSDBw/GHzx4MC6TycZlMtl4e3t7WmQy2V+uX78eOHjw4L8A+NFsn4vjuOc5jvtPAH68aDnisXHjxp++995769Kxfft2idnueVps3bp1TjZv3ry+rKwsB8CzT/zB/3b87Xgqx/8D2eEtvSevT9MAAAAASUVORK5CYII=)}.edit_frm .invoice_layout .btn.sdoc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAfGUlEQVR4nO2bZ1Dcd5qgtTOeWfuuLs59mLoPV3W7V3VTV7d1O+u1JCQroICCZdleW/Z4LM3KPluSlaNHcpQsFCwhBAooECQBAkQQUdCIDA10Aw3ddM450xHweJi5fe4DdA8gULTn7sP8q55quvtPQz+87/t73x//njPnL8dfjv+vDuBHSqXy5yqV6n9qtdqXzp8/v/Szzz7b8MUXX2w8duzYGrFYnKDX638pl8v/1m63v/D/+vd9pqO4uPjHW7Zs+cnRo0efm43i4uIfHzx48F8fPnz4b86cOfPmnj17Mvbv39994sQJ54ULF6JZWVm/Lyws/ENJSckf8/Lyxq5du/btmTNnhj7//HPNrl27ylJSUg58/fXXL+7YseNnxcXFPz569OiPiouLf/w4HD169EePA/BXj+KpBG3YsOHfzZkz5x+ef/75ZZNYGuO5555b+rOf/ew3K1asyN29e7fhypUrowKB4F8GBwfR6/WYzWasVit2ux2nw4HdbsdisWAwGNBoNIjFYu7cufOH5ORk/zvvvNP4i1/84sjzzz+/7IUXXljw3HPPPQ4JMV544YWEF154Yf5PfvKTeTMw9xG89POf//x/CIXCf/NEgjZv3vzvX3rppdPZ2dnOgoICR0FBgaOwsNCRl5fnOHHihOOjjz5yHj16NFpXV/cvSqUSq9WK0+HAZrViMpvRGwyotDpkKg1SlQaFWotWp8NgNGKxWLDbbDjsdgwGAz09PeTk5Px+x44dnn379tkzMzNtBQUF9tu3b9sLCgoeoLCw0FZYWGgrKiqy3S7It/32xEf25HOHbUVFRbaioqL4c4/DnTt3bCkpKfLCwsLlTxxBSUlJVx0OB8PDw4yMjOByubhz5w4XLlygsrIStVqNw27HZrWi0ekRSRXUiga43dpDTqOYzEYxVxvEXLkv5nqDmKxGMbeaxVR0SmiXyJAqVZhMJuw2GxaLBZFIRE5ODhkZGfT398d/7vDw8IyMjIwQCPk4fvPXHLi8gua+EqLDEUZHRxkZGYkzOjo6K99++y0ikShSV1e3/okFrVy58orNZiMajaLVarl69SrXr19HKpXGU0aqVFErGiC3pZfL9SJS60Sci1HbPcHkr7tJrRORLhCR1dRDmVBC94AcrU6HzWbFYDBQW1tLamoqDQ0NBINBIpEI4XCYSCQy5etoJIpvyEPyrU1crT3A0ZsbKLp/Dq/fxXB0mGg0SjQanVVwTHJnZ+fTCVqxYkWGzWZDIpFw/vx5SkpK0Ov1WK1WFGoN93uk3Gjp5cL9HtLv93BxEhdmYaZzrjb2Ut7ZT49MgcFoxGq10tnZSVpaGsXFxXi93riYyaKi0QlBNzfSNJhPn1nAydsbybj7CRanjuHh8eiLDj8oKSbuqQW99957/zYpKSmjsbGRM2fOUFtbi9lsxmwyIZbKKWyXkNHYS0ZTH9dbJGS19pM9iZzWfnLaJjHt+RhZrf1cax5/rZyWPhp6pKjVGiwWC/39/Vy4cIG8vDz8fv8DkTRZUKMsF3NwEJm9hYuVuzmd9z4qYy8jMUmzRNLo6OjTC5o7d+6V5ORkBAJBfAVqlwyS2ybhSpOE7LYBbncOUtCloLBbQbFIyd0eFVV9au71axAMaKmXahEMaKkb0FIj0VDRq6asR0WxSElht4LCLgUFXXJyhTKut/ST2SyhRjSAXKnCYrEwMDBASkoK5eXlf0qtidSJCTp+cyMNslto/SJUXiFqTze5Tcf4MvstumS1cTnTb2P16akEbdq06b+uWbOmtbS0FLPZjE6vp6lPxs22fjJbB7jdJedur5qKPg01/Voa5QY6VCa6NGZEGjMirWVWujRmOlQmWhRGBDI9Vf16KiQ6yno15ArlZLdJqeiWIVOosVisiERiznxzhtraOoxGEyajGZPRhMlkRq1V8sX1t7kvu4Xa14XU1Yjc3YrG102F+CKfZ71OScMlnG4HI8MjUyLpqQUBz33wwQenr169+kedTofRaByPnPYBctqllPaoqJMZuD9opENtRmKwITXa4wwY7fQb7PTqbYi0Vjo1FjrUZjrUZjo1Frq1Fnp0VvoNdjqVCvKbCrhyL5uMmmwuVmdxujSDE3cucaX8MiWC61S13ORCzlfs+vQ9Lt36mhslKeRMkHnnJDvOLqZBdguVV0i/sx6Jo45+Zy1KbxtNilw+z36dq2Wf4vbZGBkZjct5akFZWVkLv/jiC5dcLo/XnKJOGTc7ZNztVdOsNNOusdBvdKC0ulDZxlFaXcjMTnoNdrq0NtrVFlonaFGNE7vfprbQqbVzsz6bAxkrOVe6lXOl2zhXuo2U0m2klGwlpWQrqaXbOF+6jdTSbZwp+pCU4i2klmzlXMnWidstpJdvp9dci9zTSp/jHr32KsS2ckS2MqSuOroMpZwu2sj5Ozsx2lVT2oAnFmQ2m5/fu3dvbnV1NWazGZlCRYVokDzhIHd71bRrrIgNdpQ2N3qnF4PTi97pQWVz0292IjLY6dI9Ht16B1fvXeJS5S7Uni4U7vaH4+lA4XnwcaVHiMrbSb9TgNhWgchaRpe1mE5LIR3mfMT2UnosFVyq3sHxm+8i1XQ8vaC8vLxXjh8/HlKr1eh0Opolcgq75dwRKcejxuJC6/Ri9fixevyY3D6Udg/9Zhd9JucTITG7yaq7TEb1XhSeNnrtNU9Nj70asa2cbmspnZYiOsy3aTfl0mq6QYsxiw7zLXptd7nV8imfZa6nqbeEaDTCt6PfPr4gl8v1r/bs2VNeV1eH2WymX66kskdJsVhJ/aABqdWD3uXH4Qvg8Acwe4ZQ2r3IrJ6nYtDm40b9FTKq9yJ3tyK2VT01IlsF3dYShJYiOsz5tJlu0mS4Rr3uAjXqs9xVfEXx4GHuac5Q0PUFn2atpbgxjWDIT1dX9+MJun379t9/9dVXTpVKhVarpaVfQWWfmkqJhl6TE717CNdQCE8ghM0XRO30o3T4nhqVc4hbDVfJqN7LoKsFkbXiKSmny1qK0FxEmymXBv1VKpQnKZB+wg3Jx1zv+WeuiN7lUvdbXOp+iyLZfu72H+er3PVcqzhMraAiUltb++ojBZ08eXJ3ZmbmH00mEzKFEkG/mup+LfflRjSuIVyBMP5QBGcghMETQOsOoHsG9J4Q+Y3Xyajei8zVRLf17lPRZSmjw1zIfd1VyhUnuT1wiBt928ns+d9cFW8iQ/QrLna9SXrna6QJ13Fe+Ao3+j6kcjCZ0yW/Yl/K2m9Tc4788yPT68MPPyxqamrCaDTSI1NwX6ajZkBHt96BzR8iEI7iDUUw+0IYvcFnxuQLU9A8IcjZSKel9KloMxVQo06nSPYFeQMHuCnZRXbvVq73bOaK6Ndc6n6L9M7XOC9cy7mOJFLaV3C2fTlXxO9QoTjGhZoP+U3yL/rTS3Ysn3V/KC8v77/t3r3bOjAwgFarpXNQTbPCgGDQgNLhwxeOEoxGcQTCWPzPwFAYa5wIBc3XuVy9B6mzAaG5+IlpMeZRpTpP8eBRCqRHyOvfz42+HWT1fsQ18W+4LHqHC11vcF74CikdKznTnsg3bYsnWMKFrvWUyT/jZttBtpz7e2t6ycfvA3/9gKAzZ84sOnXqVESj0aBQqWiX62hRmmhQmjD5goSiI/jCUeyByNMTjGAZCqNzB5BaPYiNLjJqLnC5eg/9jno6TEVPRIshl2pVOmXyk9yRfUWB9DC5/fvi6XVFvJFL3RtI71xPascqzrYncrr1ZU62zudk63xOtS7km7bFpHeu457mFHntR3j367/x3Kr7OmEmQW9eunRp1GgwIFMoESoNtKnMtGmsOAJhQsMjuELDOEPRJ8IViuIKDWMPRNC4hug2OGhWmWlUmGhQmEmvOD8hSECbsWAc00OYeL7ZkEu16gLlirOUyk9SJPuS29Lfcqt/Lzl9H3O95wOuiH/Nxe43SetcR0rHSr5pW8zJ1vkcb36R483/QHLLS5xtT+S2dBfFPV9y4Ori6P4LK05bw4P/4QFBZ8+e3X3t2rUxk8mEVK5ErDHRrjbTpbfjDEYYigzjDj8ZnvAwzmAUnTtAj9FJq9pCs9JMs2qcFrWVi5VpXK7eg8RRR6sx/7FoNtyiRn2RckXKhKAT0wRt43rP++OCut4cT6/2FZxuW8SJlrkca/pfHG36O860L6FItp/slr18nPqibV964hYz5udnrEFpaWmnsrOzMRmNDMiV9OksdGistGutDFjcGL1BHMEInvAw3sgE4WlMetwVimLwBOgzu+jQ2uLjRZvGGqddY+NSVTqXq/fQZ6+l2ZD7UFoMuTTpb1CjvkS5IoUKxblZBH38J0HTIuhEyzyON/+S88KVFMsOkVazme3fLPruWmHy7odu4KelpaXn5ORg0OsZUKjoN9jo1NkQasfp1NnpNblQ2L2YfEEcgQjOYCyFojiDURyBCCZfEJXTR7/FTZfOTof2T68xnU6dg4zq8RrUa6uhSX/zodTrMqlWpVOhSBkXpDxHuSIlXoNuT9SgnCk16C3SO1/lXMcqzrQv5XTry1wVv03xwGGSC9/gdN4HFNzNeXQfdO7cuaOZmZn/otfrkSpUyIw2xAY73fo/0TVxKzI46DE6kZhdDFjdDFjcSMwuek1ORAbHlHMfhsjg5OpEke6xVdOgz5lCoz6HBl029dpM7qkvU6lMnUirqZTJT3FnYhXLja1iPR9yVbyJy93vcKHrdVKFa0gVriZv4GMKew7zac5qrpT9FqNFQ2trW6SmpubhnfTp06e3pqam/l6v1yFTKBk02pCYnPQYH454gkedNxO9JjfX713kctUexLYqGnTZ3Ndlc1+XRb0ukzrtVWrUl6hUnqdCeW5W7iq+oUR+nELZZ5P6oC1c79lMhuhdLna9SYbobUrln3KzbT+Hr6/mzv107A4rfp+fpqamRwv65ptvXv/iiy9GlUolgwolcqMVmcWFxPzD0W/2kFl7kYuVO5E6G8ZnKmtFvDvutJTSaS6ZiiVGrFEcv99myqNMnkz+wCFuSfaQ0/fxRCe9kRt9W6hWneJS7TY+y3yNRlEpbo8br9eL1+t9PEEnT55M2L59e7i/vx+FUonCYEZpczFgcSP9gZBZveQ33WTvpeUk528kOX9TnBP5mzh5e/w2dv9E/iaS8zaSnPsex27+iqM33iE57z1OF/6GVm0B1epUCqSHyevfz03JTrJ7t3Jn8DD3FKmcKd1I8s1NSJQd+Hw+PB4Pbrcbj8dDY2PjowUJhcL/snnzZnVraysqlQqFVo/W7kZu8zL4AyG3++gxGKjsrqJMWBbnrrCMhp5yWvoqqOgso0xYSrWwhLuthRTXXie3LJ19n2/myzO7yb17np0pS6mWXOKeJp1C6WfkDxwir38/1epvqBpI5cubr5NWuAedSYnP54/LeSJBwE/fe++97Ly8PDQaNXKVGr3dhdrhQ+nwo/oBUDp8qBx+1M4Aalcwjs0XIhyO4BkKo3EF0boCmF0+1EYrvb19CDuEfPXlUVqaWzGZDXyV+Q7VksvUaS9xR/YlxYNf0aC/SonoJJ9mriPv3hmMJh0+r2+KHJfLhdvtpqGh4dGC5syZM+f8+fMfHDly5Du5XI5coURnsaF3+dC6h/5sWL0BAsEggWAQs3cIjXsIo8uLweFGrtbQ0dFBWVkZR48exefzMRT0cvzGe1RLMhDorlClOkebMZ8bjUf4PPMN7ncVxevNdDlPLKiuru6/b9682dzU1IRCoUCtM2B2ejB4Aui9wR8UgzeI3T8uJhQK4R4af8zgGcLi8qCz2unt66OxsZGzZ8+Sk5NDNBrF43Xydc6vqZFcodNaTLu+kPSK7STf3ESvvBWfzz+jHKfTidPpxOVycf/+/ccTBPx006ZN2RcvXiQWRUabHYt3CJMv9INh9oVwDoUIBEOEQiH8gRBW//hzFo8Pi9ONUqOlubmZiooK9uzZg0gkIhwO43TbOZbzLnXSTNq1xZws2MiFO3vRGAfxT9Sb2eQ4HA6cTif19fWPJ2jOnDlzzp49m7Bx40Zna2srcrkctU6PzeXB6g9N2qb4/nAMhfAFggSDIUKhIIFgCGdg/DmbL4Dd7cVgtdPT04NAICAtLY1z587h8/kIBII43TaO5bzLlXv7+OrGW9yuO4vNYY6vVA+TE+OJBAE/fuedd86fOnUKsVjMoFyOwWzF4fXjCERwBKPfC65gBG8wPBE1wbgcTzA8fk4gjMPjxeryMKhQUl9fT2lpKYcOHaKvr49QKEQgEMDptvFV1tvsTV9KfVcBHo8bn88XT6uYoJnk2O12HA4HAoHg8QXNmTNnTkNDw9+99dZb+vz8fPr6+lAqVVhsNlz+AO7Q1IndF4rgC4XxhCK4Q9HZp/tQFE9ofEdyKBQmGPqTmGAwSCAUwheKxM91en043B7UWh1NjY2Ul5fzySef8NFHHyGVSnE4HNhsNkxmIxWN2bSKqrFYLJjNZkwmEyaTCaPRGMdqtT4gx2azYbfbn1zQnDlz5rz//vu7t2zZMnLv3j0kEglqjQab04l7KIgvMoIvOoovMsJQOEowFCIYChEIhQiEwgyFwgyFIgyFI+O3oTCBiXOCoSCh4J8ITnyPPzKMLzo6vhPg8+N0e9AbTbS3t1NZWcn58+dJSkpi/vz5bN++HYFAQHd3N52dnXR1dtHV2YVQKEQoFNLR0fEACoUiLigm55kEvfvuu//pl7/8ZcWRI0eoqqqip6cHjVaLze7AMxRgKDpKYPjbcaIjBCNRguFYZIQIBscjIzSN4ISUYChEMBwhGBmOv44/Mozb68PpcqM3mhAKhVRWVpKTk8Nbb73FwoULSUhIYN68eezevRuJRILFYnkgYgwGAwaDAb1ej16vR6fTYTabsdvtU+RYrVZsNht1dXVPLmjz5s3PL1iwIH3//v0cOXKE8vJyRCIRKrUaq9WK1z9EIDJMePR3UxkZJTw8Qig6TCgSJRSJTCI6TnRk/LyJ7wmNfIs/GMbt9eJ0udBotbS0tFBeXk5mZibvvvsuixYtipOQkMD8+fM5dOgQ3d3dKBQKBgcH48hkMmQyGVKpFKlUysDAADqd7gE5MZ5a0Jo1a9K6u7s5dOgQn3/+OWVlZXR0dDA4OIjJZMbpcuEPBAmPjDL87XcM/+730/hu/PEY056Pjv6OYGQYr8+Py+3GZncgVyhoaGjg7t27XLlyhV/96lcsWbKExYsXT2HBggXMmzePrVu3IhAIEIlEdHd309XVRVdXF52dnVNQKBRTxFgsFiwWy7MJWrVqVZrD4UCr1ZKcnMz+/fvJzs6mrq4OsViMWq3GYrHgdnvwDw0Rjg4zPPo7Rn73e779boxvfz+V0e/GGP72OyIjowRCYbxeH263B7vdgVqjQSgUUlVVRUlJCampqbz55pssWbJkRmKS5s+fz/79+5FKpVit1hmLtMFgwGw2xwXF5JjNZiwWy7MJil2jaLfbuXTpEh988AHnzp3j7t27NDY2IhaLUSqV8Rx3uz34fD78Q0PxkSEQDDIUCODz+yekuHE4HFgsFlQqFV1dXdTU1FBWVkZ2djZ79uxhzZo1LF26dFZiohYuXMi8efPYuXMnAoGArq6Zi/Xg4OADcmLU1tY+taDzNpstfmWXz+ejoqKCffv2cejQITIyMiguLubevXu0trbS29uLQqlEo9HEr5WOrRQWiwW9Xo9Gq0WpVCISiWhoaKCyspLS0lJu377NqVOn+OCDD1i+fDnz589n6dKlJCYmzshkWQsXLmTu3Lls27aN1tbWeB2K1SCpVIpGo4kLiokxmUyYzWbu3bv35IJ27dr110lJSakxQZOvDTQajeTk5LB161b27NlDamoqhYWFlJaWUllZSW1tLQ0NDTQ1NdHS0kJLSwvNzc0IBAKqq6upqKigrKyMkpISrl+/zmeffcb777/PJ598QlVVFRcvXmTp0qUkJCSwdOlSli1bFmc2YQsXLmT+/PkcPHhwfMXVaFCpVKhUKpRKJXq9/gE5RqMRk8n0/QuKRqMMDQ0hlUrJysrit7/9LTt27ODAgQOcOnWKtLQ0bt68SWFhIYWFhRQVFVFcXEx+fj4ZGRmkpqZy7NgxPv74Y7Zt28bBgwfJycmhtbUVkUiEUCjkzJkzJCYmkpCQQGJiIsuXL58iajqJiYm8/PLL8cJdXV1NW1sbra2ttLS0IJVKH5ATE1RTU/NsgqZfXRqJRPD7/QQCAUZHR3G5XPT29pKfn8/hw4fZuXMnBw4c4ODBg+zcuTMuL3Z/586dpKamIhAIkMvlOBwOXC5XvImLpeSNGzdYsWIFCQkJLFu2jBUrVrB8+fJZWbZsWVzSrl27EIvF6HS6eMrPVLyNRuOzCbLb7VPETBY0NDTE8PAwkUgkfkW8Xq9HLBbT1NREZWUlt2/fJjc3l9LSUgQCAUKhEIlEgkwmQ6FQIJfL433L9LrR19fH2bNn4+kWkzQbMVGLFi1i/vz5HDhwAKlU+kAzObmRNBgMP6ygkZGReNpFIhEMBgMDAwNIpVJkMtkDzdvAwAD9/f309/cjkUji9PX1xent7Y0jFotJT09n2bJlJCQksHz5clauXDkjk2VNltTf34/ZbJ6xyzYYDFRXV//5BMWm7NkYGhrC7/fj9/vx+XzxyXv69D15189ms5GVlRWXtGLFCpKSkkhKSnqorJik/fv3I5FIMJlMD4wger3+zysoVtAnE5o2nz2NLLvdTnZ2NsuXL49LWrVqVVzUTKxcuTIuad++ffT19WE0GuNynlnQ6tWrH1vQ9yVntiiKRVJM0oIFC1i5ciWrVq2alZioxYsXM2/ePPbu3Utvby96vR6tVotWq0Wv11NTUxOuqqpa970Iiq1mQ0NDMwqKvfnJAmISHpVK0/drZhouTSYT165dY9myZSxYsICkpCRWr149I5NlLVmyJB5JEokkPmZYrVauXbvWVVRU9LdPJGjDhg0/nS5o8lI/XVAsWjQaTbzA9vT0xBGLxXFEIlGc7u7uOLFBc/qwGRsdYrS1tXHy5EmWLFkSl7RmzZo4s8mKSfrkk09Qq9X4fD6qqqrUJ0+eXPREch4WQZMbxZkEWa3WePhqNBo0Gg1qtTrO5O42hkKhiC/5crl81m2L2NbFwMAAEomEixcvsnz5chYuXMiqVatYu3btFFHTWb169RRJxcXF1pSUlFVPLGemCAqHw/j9fsLh8AOCJtcbt9s947bC5C72UZtbOp3ukZLVajVyuZy0tLT4PLZ69WpeeeUV1q5dOyuxITghIYElS5a0V1ZW/udnEXTObrcTjUYxGAzU19ejVqunpNjw8PAUQQqFAqFQ+EBqzLQN2t7eHqetrS0+GsSIzXGxWW4mGhoaOHXqVHwei0majZioxMRE5s2b98c1a9bk9/T0/PyZBcnlcpqbmzEYDITD4SmCJq9UXq93xv8eTC+2TxJZj4oqhUJBRkYGy5YtY+HChaxZs4Z169ZN4ZVXXonfxkQlJiYyd+7cP65duzbviSVNFxQIBHA6nYRCoXiK+f1+otHolCU8thxPlzF5/2X6fxwmp9hMQrRabXzVme0/F1qtlitXrsQlrV27lldffZVXX331AVmTpcUkrVmz5voTfTQ8JsjhcEz5hF+M6YJi/Y1CoZh123O2tJsp1SanXEdHB1qtdkY5sUgzGAzodDpycnJYsWJFXNL69evjomZi3bp1JCYm8uKLL0aPHTu24bEFbdmy5SerVq1KmS4otpL5fL7450gnN39Op3PW9HlUCsWiJpZCk9PIaDTOKif2GgaDAbfbTX5+flzSK6+8wvr16x/K2rVrmTt3LsePH09+bEFHjx59brKgyb1QKBSKd78xQdObw8eZsx63Vs2UmjOtfnq9Pv6Hu3XrFsuXL+fll19m3bp1vPbaazOyfv161q1bx9y5c8eOHz++66kFTR8dvF4vPp9vSsF+lJjJHfNsXfNMRfxxWoPY2OD1ehkaGsLtdpOVlfV/li5d+sdFixaxbt06Xn/99TivvfZa/HbFihUsXrxYXF5e/vjddEyQ3W6fcaaaLOhxomYmOTExTypnejGPCTIYDHg8nvjPzs3N7ViwYEHySy+95Fu8eDGvvvoqb7zxxhRRSUlJJCYmKoqLi5c8tpzZBE1erWK/SCgUeiY5D1v+H0eOVqudIih2WV1FRYXs4MGD84Afv/3229v+8R//MS7pn/7pn3jjjTdYuXIlS5culT+xnJkETZ/GJwt6nGn8UXIepy+aTU7sa6PRiNvtpq6uzvDpp5+ujL0X4LkNGzZsefHFF10LFiwgMTGRxYsXs2TJEtlTyZksyOFwxDfCHifFnnZif1gDOVvtmY7ZbKa5udnx5Zdfvjn9YwXAcydOnFiTlJR0OykpqWnVqlUXi4uL//6p5Ey84F8tW7YsuaWl5Q8ikWisu7t7rKurK05nZ+dYX1/fWG9v71hHR0ec9vb2sfb29rG2traxtra2sdbW1jgtLS1jLS0tY83NzWPNzc1jTU1NY01NTWONjY1jjY2NYw0NDWMNDQ1j9+/fj1NfXz9WX18/JhAIxgQCwVhdXd2MCASCPxQVFXm+/vrr3wA/mu192e32F+x2+38EfvrUcmLH5s2bf7F9+/ZXZ2LXrl1xZjvn+2Lr1q2P5KOPPlp/5MiRhcBPnvmN/+X4y/G9HP8XQHLbNqTL5PYAAAAASUVORK5CYII=)}.edit_frm .invoice_layout.freeze::before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(50,50,50,.3);user-select:none;cursor:wait;z-index:10;pointer-events:none}.edit_frm .invoice_layout.freeze::before tr:hover>td.aux>.axf{display:inline-flex}.edit_frm .invoice_layout.freeze::after{content:"";position:absolute;top:50%;left:50%;width:3rem;height:3rem;margin:-1.5rem 0 0 -1.5rem;border:.4rem solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;z-index:11;pointer-events:none;animation:.8s linear infinite fis_spin}.modal-body .lstfrm{display:block;position:relative}.modal-body .lstfrm .li{display:inline-block;background-color:#fff;border:1px solid #e0e0e0;padding:1.7rem .5rem .5rem;border-radius:.2rem;cursor:pointer;position:relative;margin:0 .5rem .5rem 0;box-shadow:1px 1px 3px rgba(50,50,50,.1)}.modal-body .lstfrm .li .lbl{position:absolute;background:#727272;color:#fff;font-size:.8rem;left:0;top:0;right:0;padding:.2rem;border-top-left-radius:inherit;border-top-right-radius:inherit} \ No newline at end of file +.invfrm>table td.sh_s,table.invtbl td.sh_s{color:#bf4b06}.invfrm>table td.is_cc,table.invtbl td.is_cc{background-color:#777!important;color:#fff!important}.invfrm>table td.is_pyd,table.invtbl td.is_pyd{color:#60cc08}.invfrm>table td.is_due,table.invtbl td.is_due{background-color:#f2f23f!important}.invfrm>table td.is_ovd,table.invtbl td.is_ovd{background-color:#de4e09!important}.invfrm>table td.is_rem,table.invtbl td.is_rem{background-color:#ce1900!important}.invfrm>table td.av,table.invtbl td.av{border:2px solid red}.invfrm>table td.av+td.vsel select,table.invtbl td.av+td.vsel select{display:block}.rfrm{min-width:200px;min-height:200px}table.if{border-collapse:collapse;margin:1rem 0;background:#fff}table.if tr:nth-child(2n+1) td{background-color:#f9f9f9}table.if tr.title td{background-color:#56a532;color:#fff}table.if tr.title td .eid{font-weight:700;margin:0 1rem 0 .5rem;font-size:140%}table.if tr.title td .nme{font-size:110%}table.if tr.shd td{font-style:italic;color:#bbb;background-color:#fff!important;font-size:90%}table.if td,table.if th{padding:.2rem .35rem;border:1px solid #ddd}table.if td.currency{text-align:right;white-space:nowrap}table.if td.num{white-space:nowrap}.pdfpreview .pdfp.ph>.note{background:#fff;display:inline-block;padding:3rem;margin:.2rem 0}@keyframes fis_spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.edit_frm .invoice_layout{margin:2rem 2rem 2rem auto;width:50rem;border:1px solid #eee;padding:25.5rem 3rem 3rem;box-shadow:4px 4px 9px rgba(50,50,50,.3);position:relative}.edit_frm .invoice_layout .axf{font-size:1rem;display:none;position:absolute;right:100%;height:100%;min-height:1.5rem;min-width:2rem;background:#f0ffe2;border:1px solid #ccc;border-top-left-radius:.2rem;border-bottom-left-radius:.2rem;top:0;width:auto;max-height:1.5rem;overflow:hidden;padding:.1rem}.edit_frm .invoice_layout .axf>.ibtn{cursor:pointer;height:100%;width:1.5rem;display:inline-block;position:relative}.edit_frm .invoice_layout .sndfrm{position:absolute;font-size:.7rem;color:#727272;top:4.7rem;left:3rem;width:auto;height:auto;user-select:none}.edit_frm .invoice_layout .emailfrm{position:absolute;font-size:.7rem;top:.5rem;left:.5rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .emailfrm .content{color:#000}.edit_frm .invoice_layout .emailfrm .content:empty::before{content:"-";min-width:5rem;height:1rem;display:block;position:absolute}.edit_frm .invoice_layout .emailfrm:hover div.axf{display:block}.edit_frm .invoice_layout .admfrm{position:absolute;right:3rem;top:9rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .admfrm .ahd{display:block;color:#727272;margin:0 0 .2rem}.edit_frm .invoice_layout .admfrm .content{color:#000}.edit_frm .invoice_layout .admfrm .content:empty::before{content:"-";min-width:5rem;height:1rem;display:block;position:absolute}.edit_frm .invoice_layout .admfrm:hover div.axf{display:block}.edit_frm .invoice_layout .ctpfrm{position:absolute;right:3rem;top:14rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .ctpfrm .ahd{display:block;color:#727272;margin:0 0 .2rem}.edit_frm .invoice_layout .ctpfrm .content{color:#000}.edit_frm .invoice_layout .ctpfrm .content:empty::before{content:"-";min-width:5rem;height:1rem;display:block;position:absolute}.edit_frm .invoice_layout .ctpfrm:hover div.axf{display:block}.edit_frm .invoice_layout .adrfrm{position:absolute;left:3rem;top:6rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .adrfrm:hover div.axf{display:block}.edit_frm .invoice_layout .locfrm{position:absolute;left:3rem;top:18.5rem;width:auto;height:auto;cursor:pointer}.edit_frm .invoice_layout .locfrm .ahd{display:block;color:#727272;margin:0 0 .2rem}.edit_frm .invoice_layout .locfrm .content:empty::before{content:"-";min-width:5rem;height:1rem;display:block;position:absolute}.edit_frm .invoice_layout .locfrm:hover div.axf{display:block}.edit_frm .invoice_layout .tfrm,.edit_frm .invoice_layout>div.title{position:absolute;left:3rem;top:15rem;font-size:2rem;cursor:pointer}.edit_frm .invoice_layout .tfrm:hover div.axf,.edit_frm .invoice_layout>div.title:hover div.axf{display:block}.edit_frm .invoice_layout div.fnote{margin:1rem 0}.edit_frm .invoice_layout p+table.invi,.edit_frm .invoice_layout table.invi+p{margin-top:2.5rem}.edit_frm .invoice_layout table.invi{border-collapse:separate;border-spacing:0;border-color:#fff;width:100%;max-width:100%}.edit_frm .invoice_layout table.invi>tbody>tr.hidden{display:none}.edit_frm .invoice_layout table.invi>tbody>tr:nth-child(2n+1) td{background-color:#fbfbfb;border-color:#fbfbfb}.edit_frm .invoice_layout table.invi>tbody>tr.title>td{background:#fff;border-color:#fff;border-top:1.5px solid #000}.edit_frm .invoice_layout table.invi>tbody>tr.sethdr td.txt{font-weight:700}.edit_frm .invoice_layout table.invi td.currency,.edit_frm .invoice_layout table.invi td.num,.edit_frm .invoice_layout table.invi th.currency,.edit_frm .invoice_layout table.invi th.num{text-align:right;white-space:nowrap}.edit_frm .invoice_layout table.invi td.keep,.edit_frm .invoice_layout table.invi th.keep{white-space:nowrap}.edit_frm .invoice_layout table.invi table.stbl td,.edit_frm .invoice_layout table.invi table.stbl th{padding:.3rem .5rem}.edit_frm .invoice_layout table.invi table.stbl td:first-child,.edit_frm .invoice_layout table.invi table.stbl th:first-child{padding-left:0}.edit_frm .invoice_layout table.invi table.stbl td:last-child,.edit_frm .invoice_layout table.invi table.stbl th:last-child{padding-right:0}.edit_frm .invoice_layout table.invi>tbody>tr>td,.edit_frm .invoice_layout table.invi>tbody>tr>th,.edit_frm .invoice_layout table.invi>tfoot>tr>td,.edit_frm .invoice_layout table.invi>tfoot>tr>th,.edit_frm .invoice_layout table.invi>thead>tr>td,.edit_frm .invoice_layout table.invi>thead>tr>th,.edit_frm .invoice_layout table.invi>tr>td,.edit_frm .invoice_layout table.invi>tr>th{border-width:1px;padding:.3rem .5rem;border-left:none!important;border-right:none!important}.edit_frm .invoice_layout table.invi>tbody>tr>td.aux,.edit_frm .invoice_layout table.invi>tbody>tr>th.aux,.edit_frm .invoice_layout table.invi>tfoot>tr>td.aux,.edit_frm .invoice_layout table.invi>tfoot>tr>th.aux,.edit_frm .invoice_layout table.invi>thead>tr>td.aux,.edit_frm .invoice_layout table.invi>thead>tr>th.aux,.edit_frm .invoice_layout table.invi>tr>td.aux,.edit_frm .invoice_layout table.invi>tr>th.aux{width:0;max-width:0;min-width:0;position:relative;padding:0}.edit_frm .invoice_layout table.invi>tbody>tr>th,.edit_frm .invoice_layout table.invi>tfoot>tr>th,.edit_frm .invoice_layout table.invi>thead>tr>th,.edit_frm .invoice_layout table.invi>tr>th{text-align:left;font-weight:300;background-color:#fff;border-color:#fff;font-size:.8rem;font-style:italic;border-top-style:none;border-bottom-style:none}.edit_frm .invoice_layout table.invi>tbody>tr>td,.edit_frm .invoice_layout table.invi>tfoot>tr>td,.edit_frm .invoice_layout table.invi>thead>tr>td,.edit_frm .invoice_layout table.invi>tr>td{vertical-align:top;font-weight:400;background-color:#f8f8f8;border-color:#f8f8f8;border-top-style:solid;border-bottom-style:solid;user-select:none}.edit_frm .invoice_layout table.invi>tbody>tr:hover>td.aux>.axf,.edit_frm .invoice_layout table.invi>tfoot>tr:hover>td.aux>.axf,.edit_frm .invoice_layout table.invi>thead>tr:hover>td.aux>.axf,.edit_frm .invoice_layout table.invi>tr:hover>td.aux>.axf{display:inline-flex}.edit_frm .invoice_layout table.invi>tbody>tr.isum>td,.edit_frm .invoice_layout table.invi>tfoot>tr.isum>td,.edit_frm .invoice_layout table.invi>thead>tr.isum>td,.edit_frm .invoice_layout table.invi>tr.isum>td{border-top:1px solid #000;background:#fff}.edit_frm .invoice_layout table.invi>tbody>tr.sep>td,.edit_frm .invoice_layout table.invi>tfoot>tr.sep>td,.edit_frm .invoice_layout table.invi>thead>tr.sep>td,.edit_frm .invoice_layout table.invi>tr.sep>td{background:#fff}.edit_frm .invoice_layout table.invi>tbody>tr.tsum td,.edit_frm .invoice_layout table.invi>tfoot>tr.tsum td,.edit_frm .invoice_layout table.invi>thead>tr.tsum td,.edit_frm .invoice_layout table.invi>tr.tsum td{font-weight:600;border-top:1px solid #000;border-bottom:1px solid #000}.edit_frm .invoice_layout table.invi .sortable>tr>td.aux .ico.move{background:url(data:image/svg+xml;base64,PHN2ZyBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAzMiAzMiIgaWQ9IkxheWVyXzQiIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48Zz48cG9seWdvbiBmaWxsPSJub25lIiBwb2ludHM9IjE2LDMxIDgsMTggMjQsMTggICAgICIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZT0iI0RERERERCIvPjxwb2x5Z29uIGZpbGw9Im5vbmUiIHBvaW50cz0iMTYsMSA4LDE0IDI0LDE0ICAgICAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIyIiBzdHJva2U9IiNEREREREQiLz48L2c+PC9zdmc+) center center/contain no-repeat border-box}.edit_frm .invoice_layout table.invi .sortable>tr>td.aux .ico.move:hover{background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMzIgMzIiIGlkPSJMYXllcl80IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAzMiAzMiIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PGc+PHBvbHlnb24gZmlsbD0ibm9uZSIgcG9pbnRzPSIxNiwzMSA4LDE4IDI0LDE4ICAgICAiIHN0cm9rZT0iIzAwMDAwMCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiBzdHJva2Utd2lkdGg9IjIiLz48cG9seWdvbiBmaWxsPSJub25lIiBwb2ludHM9IjE2LDEgOCwxNCAyNCwxNCAgICAgIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9nPjwvc3ZnPg==)}.edit_frm .invoice_layout table.invi .sortable>tr.sortactive>td.ico{cursor:move}.edit_frm .invoice_layout .btn{background:url("") center center/contain no-repeat content-box #fff;position:absolute;top:.2rem;right:.2rem;height:3rem;width:3rem;padding:.1rem;z-index:11}.edit_frm .invoice_layout .btn.sprev{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAaz0lEQVR4nO2caVAbd97neSbObPJs7VE7z4vU82Krdp+t2qmt3apnNhObIzanwfjKZBJPJpXJU45rbGcSH4mvCZM4g08yPjC2s/gCJjG2sfEB2BhkA+ISoPtAal3dUnerW/fRkgAnYbLz3RfQHQHCHHby7Ivpqk9JdLcE+vh//H6//1/OyPjb8bfj/6sDwI8IgnjBarX+T4fD8dKpU6fyPv744w379+//zYEDB0pVKlUWSZI/M5vN/8Rx3PP/2n/vEx2NjY3PbNmy5dny8vIls9HY2PjMnj17/u1HH330X48dO/bazp07q3ft2jV05MgR75kzZ5I1NTXfNDQ0/OXmzZvf1tfXj1+4cOHRsWPHIp988ol9+/btt0+cOLH74MGDL77//vs/aWxsfKa8vPxHjY2Nz8yH8vLyH80HAH83F4sStGHDhv+QkZHxv5977rmCFPJElixZkveTn/zkX4qKii7v2LGDOnfu3JhMJvvr8PAwSJIETdNgWRYcx8HL8+A4DgzDgKIo2O12qFQq3Lhx4y+HDx8Ov/HGG50//elPy5577rmC559/PnvJkiXzISuVZ599NvPZZ59dloalc/DSCy+88D8UCsW/W5CgjRs3/seXXnrps9raWu+1a9f4a9eu8Q0NDXx9fT1/5MgRfvPmzd7y8vJke3v7XwmCAMuy8PI8PCwLN02DpChYHU6YrHYYrXZYbA44nE5QLhcYhgHn8YDnOFAUBbVajbq6um/ef//9wIcffshdunTJc+3aNe7q1avctWvXZtDQ0OARuX79uuf69eue1HOp5+fixo0bnhMnTpgbGhoKF9yCiouLz/M8j5GREYyOjsLn8+HGjRs4c+YMWlpaYLPZwHMcPCwLu5OE0mhBm9KAqz1q1HWqcKlThfMdKpx7qMLFDhVqOlX4Uq5C84AOfToTjIQVbrcbnMcDhmGgVCpRV1eH6upq6PV66feOjIzMyujo6JyMjY3NyqNHj6BUKhPt7e3rFyxo5cqV5zweD5LJJBwOB86fP4+LFy/CaDRKXcZIWNGmNOBytwb/54ESle1KnBRpG5ok9fkQKtuVOC1ToqZLjdsKHYYMZjicTng8LCiKQltbGyorK9HR0YFYLIZEIoF4PI5EIjHjeTKZTEvqtbkEDwwMLE5QUVFRtcfjgU6nw6lTp3Dz5k2QJAmWZWGx2fFQbcSfuzU481CN0w/VOJvCmVlId8/5Tg2aBvRQmyygXC6wLIuBgQFUVVWhsbERwWBQEpMqZzZB85EjXlu0oLfeeuvfFxcXV3d2duLYsWNoa2sDTdOg3W6ojGY09OlQ3alBdZcWF7t1qOnRozaFuh496npTmHZdpKZHjwvyifeq69aiQ22EzWYHwzDQ6/U4c+YM6uvrEQ6H5916Unlc6xkZGcHY2NjiBS1duvTc4cOHIZPJpBmoTzeMy706nOvSobbXgKsDw7g2aEHDkAWNSgJ31Fbc1dpwX2+HzODAA6MDMoMD7QYHWnV2NGtsuK22olFJoGHIgoZBC64NmnFZYcLFbj0uyXVoVRpgJqxgGAYGgwEnTpxAU1OTJGexYsTzqa1n0YLefvvt/1JaWtpz69Yt0DQNJ0miS2vCF716XOox4OqgGXc0NjRr7WjVO9BpptBvdWPQTkNpp6F0MLMyaKfRb3Wj2+KCzOhEi86BJq0dt9RWfNlvQk2PHk2DBhgtBGiaxtDQED777DPcv38fLpcLFEU9FpfLNYNAIDBFzhMJArBk06ZNn50/f/5bp9MJl8s10XL6DKjrM+KW2op2E4WHwy7022joKA+MLk7C4OKgpzhoSA+UDhYDdgb9Nhr9NhoDdgZDDgZqJwsd5YHexUHpZNFhdkFmonDf6MTVQTPqeg1oVRphnpT08OFD7N+/H83NzdBoNFCr1fNGo9GApum0s9+iBNXU1OTs37/fZzabpTHn+oAJX/SbcEdjg5yg0WdnoHfxIFgfrJ4JCNYHE+2FhuIw6PCgz8agZ5Ju6wTiz702Bv12FirSAy3FQWFn0W2l0WNj0GF24YaSwNWBYXRohmGz20GSJGpra1FdXQ2/349IJJKWaDSalng8/nQE0TT93AcffHD53r17oGkaJosVzcph1CuGcUdjQ5+dhYriQHj8IL1BUN4gSG8AVo8fetoLJcVh0PmEkBy6CDduqgg0Dpmh0JtBkiRUKhUOHDiApqYmGI1G6PX6eWEwGMCybNrYacGC6uvr1xw6dEiw2WxwOp2Q68xoGDLjhpKYaDWMDw5vEGwgDDYQhtsfAsEFoKd90Lq9TxWZiUSjisA9tRkmswUkSaKhoQFHjhyByWSCw+GYN36/f4qYRY1BPp/v73fu3NnU3t4OmqahNxNoURNoVBF4MEzByAZA+sLgQ1Hw4SjoQAQEF4SJDXwvaNw+tOqduKu1oV9vhsPhgF6vx8GDByGXyxEMBuH3++ckEAggFovNGmXPW9DVq1f/+Y9//KPXarXC4XCgW29Bi9aGFp0dGrcXpD8CX0RAICrAE4rB5g2D4EPfK312Fvf1Dsh0VhjNFjgcDtTU1KCiogKDg4MYGhqaF263e0rLWZSgo0eP7rh06dK3brcbJgsBmd6Ge3oHHppdsPsi8EXjCAsJeKMCqEAUDn8Uzu8ZAxtAu9EJmcGBIeOEoLa2Nhw4cEDqOj6f77H4/f4pLWj6YK1QKOYW5PP5/v63v/3t9a6uLrhcLqhNFjw0OdFqcGKI5OEJC4jGkwgKCdAhAa5g7AeBDEQhJ2jIzRT6jDYQhBVGowGHDh1Ca2srSJJ87NjjdDrhdDqlMShdADkvQfX19f9tx44drMFggMPhwMCwDXILBdkwBYIPIRRPIpZMgo/GwYSfgEgc7CRMWIArFAM1KcIVjIEOC99dj0y8RkXx6LG40D3shMlsAUEQOHv2LM6cOTOvGcxgMMDj8cwQJOZtfX19idbW1scLOnbs2PKKioqE3W6HxWpFn9mJbsKNDsINdygGITmKUDwJLppYPLEEmEgcTn8URjYAJcWj3+FBn51Fr51Bv4PFIMlBz/jh8EfBROLgY0mYPEHICRf6rS5ohwnYbDbcvHkTp0+fRiAQmDX2iUajiMViUkVgerohXuvt7Z2XoNc+//zzMRdFwWQhoCAo9Fpp9NpZ8NE4hJFR+IQReIXkgvAJSfiEEXDRBOy+CIYoHnIrjU6LGw8n6Zgk9We5lcYQycPChWDhQ+i10hiy01CZrbDbbWhtbUVZWRkUCgW0Wi00Go2EVqudAcMwU1pOLBaTAst5CTp+/PiOCxcujLvdbhjNBFR2N/psNAZJDt5YApHECPzxhRGIj8AbS8Lpj0Lt8qLHxkyMJ9b50UXQ6LbS6LOz6LczUDsZqC12WK1WyGQyfPTRRzCbzXC73VPyLrfbLTE9F0skEpKccDiMSCSCnp6euQVVVVVV1NbWwu1ywWAmoHVOpAJ9DhYGxg9XMAY+lkAgPoJgYpL4NFLO+4QkqEAUWtqHfodHSi967ezCsLHosTFQOFhoSQ80hBMEYYFcLkdZWRmsViu8Xu8UxNkr9VwkEpkhZ6GCTtfV1YEiSRgsVugpDwacHigcEww4OWjcPli4INyhGPhoAt6Y2IWS8MaS4KMJuEMxWL0h6Bk/Bp0c+h3fvceTMOicSGy1VhLDw8OQy+XYs2cPZDIZVCoVlEollEolVCqVhHhOqVTC5XJNkRMKhRAKhRAOhyGXy+cWdPLkyfJLly79lSRJGC1WmFweqCgOQ+R3DE4+KikeapcXOtoHA+uHgfFDR/ugcXuhpPgp9z4pqsn3U1McTG4OehsJo9EImUyGffv2gSRJhEIhBINB6UMHg8Ep+P1+BIPBGXLE18xL0Geffba1srLyG5J0wmQhMOzyQOf2Qu16PKpJ5rpvMejcPti5ANQuL/RuLyw0D4PNCZPJhLa2NuzZswdqtRoWiwUWy8T0Lz4XMZvNMJlMcLvdM+SIgrq6uuYW9Kc//ekX+/fvHyMIAsMWAmYXCxPjg47+18PJB0H7Q9DSPpgYHwiGh9Fqh8lkQmtrKz755BNotVqYzWaJ4eFhidSM3+VySYJEOYFAAMFgcH6Cjh49mvXee+/F9Xo9LAQBC0WD8PhgYPww/sAYGD8ILoBAOArKF4KB8cPK+mBlvDBaCCiVStTX16OyslKqDYmDrighEAhIAzXP8/D7/TPkiIlsZ2fn3IIUCsV/3rhxo62npwdWqxUWBwkH54fZE8TwD4yFC4IPRRCJxmDjQxj2BEFyfhBuDwxGI/r7+nD48GFUVVVNqS6Kg/PQ0BAGBwcxODiIgYEBDAwMwG63S4JEOQsSBODHb731Vm19fT3sdhvMVhtIzgcbHwLBh2H9ASD4EKx8GEwgglgsBl84Cisfho0PwsX7QZBuqFQq9PX14dNPP4VcLgfHcWBZFgzDwO12g6IokCQpIeZiHo9nhhwxke3o6JhbUEZGRsapU6c2lZWVfW02m2G2EHAyHpC+EBz+yA8GG4wiGoshGouBDkZg90fg8gVB8X6YbXb09/fj9u3bKC8vRygUkoI/QRAQjUal0mtqd0vXclIz/XkLam9v/+8bN26ku7q6YLFYYHNSoL0BUIEoyGDse4UKxsCFJ8QIggB/ZOIcFYiA8QXgZDlotFp0dnbi+PHjqKurW5CY6XJSg8qHDx/OTxCAH7/99tu1Z8+ehdiKXB4OTDACd0j43qBDArwRAdGYAEEQEI4KYMMT15hACIzXD8LugFwuR3NzM3bu3AmlUol4PP5Ecnieh9frxYMHD+YnKCMjI+P48eNZv/nNb7w9PT0wm82wOUl4fAGwKWWIpwkfERCKxhCLCRCEGKIxAd7oxDVPKArOHwTFclCr1ZDJZKiqqsLJkycRCoVmyJke48wlR2RBggA888Ybb5yqqKiASqXCsNkMimbBB8PgownwseRTwRdLIBiLT7aamCQnEJsocfDROPhAEKwvgGELgQcPHuDWrVvYu3cvtFpt2q71OEHp5HAcB57nIZPJ5i8oIyMjo6Oj43+9/vrr5JUrV6DVakEQVjAeD3zhKPzC1Iw9JCQQEuIICAn4heTs2b2QRECYqEhGhDhiwndiYrEYooKAkJCQ7vUGQ+D9AdgcTnR1dqKpqQn79u3D5s2bYTQawfM8WJaVYBhmYu8ATc/I5F2TGyKmy/F4POA4buGCMjIyMt55550dW7ZsGb1//z50Oh1sdjs8Xi/8kRhCiVGEkmMIJUYRiScREwTEBAFRQUBUiCMixBEREojEExOPQhzRyXtiQgxC7Dtik68JJ0YQSo5NVAJCYXj9AZAuN/r6+tDS0oJTp06huLgYmZmZeO+99yCTyTA0NCTFOQqFQqK/v38GFotFEiTKeSJBb7755j/87Gc/ay4rK8Pdu3ehVqthdzjg4XgEIlFEkmOIjjyaIDmKWCKJWFxsGYJUrROmEZuUEhMExOIJxBIj0vuEEyPwB0Pw+vwgXW4oFAq0tLSgrq4Or7/+OnJycpCVlYVly5Zhx44d0Ol0UvyT2mLENfrUWIimaXAcN0UOy7LweDxob29fuKCNGzc+l52dfXrXrl0oKytDU1MTlEolrDYbWJZFMBxBNDGC+NhXUxkdQ3xkFEJyBEIiCSGRSCE5QXJ04r7J1wijjxCOxeEPBuH1+WB3ONDd3Y2mpiZcunQJb775JpYvXy6RlZWFzMxM7N27F0NDQ7BYLFNyMJPJBJPJBKPRCKPRCIPBAKfTOUOOyKIFlZaWVg0NDWHv3r345JNPcPv2bfT392N4eBhuNw2vz4dwNIb46BhGHn2Nka++mcbXE+dFpl1Pjn2FWGIEwVAYPr8fHo6H2WJBR0cH7ty5g3PnzuHXv/41cnNzsWLFiilkZ2dj2bJl2Lp1K2QyGZRKZdoUQ8RisUwRwzAMGIZ5MkElJSVVPM/D4XDg8OHD2LVrF2pra9He3g6VSgWbzQaGYeD3BxCORBBPjmBk7CuMfvUNHn09jkffTGXs63GMPPoaidExRIU4gsEQ/P4AOI6HzW6HQqHA3bt3cfPmTVRWVuK1115Dbm5uWkRJmZmZ2LVrF4xGI1iWTTtIUxQFmqYlQaIcmqbBMMyTCRL3KHIch88//xybNm3CyZMncefOHXR2dkKlUoEgCKmP+/2BiUpdJCKlDNFYDJFoFKFweFKKHzzPg2EYWK1WDA4OorW1Fbdv30ZtbS127tyJ0tJS5OXlzYooKicnB8uWLcO2bdsgk8kwODiYdrAeHh6eIUekra1t0YJOeTweaWdXKBRCc3MzPvzwQ+zduxfV1dVobGzE/fv30dPTA41GAwtBwD65XUX8VxM3fJIkCbvDAYKYKFl0dHSgpaUFt27dwtWrV1FRUYFNmzahsLAQmZmZyMvLQ35+flpSZeXk5GDp0qV499130dPTI41D4hhkNBpht9vThgM0TeP+/fsLF7R9+/Z/U1xcXCkKSt0b6HK5UFdXh61bt2Lnzp2orKxEQ0MDbt26hZaWFrS1taGjowNdXV3o7u5Gd3c35HI5ZDIZ7t27h+bmZty+fRs3b97ExYsX8fHHH+Odd97Bvn37cPfuXZw9exZ5eXnIyspCXl4eCgoKJGYTlpOTg8zMTKnKaLdPrH5YrVYQBAGSJGfIEVdAnrqgZDKJSCQCo9GImpoa/P73v8f777+P3bt3o6KiAlVVVfjiiy/Q0NCAhoYGXL9+HY2Njbhy5Qqqq6tRWVmJAwcO4He/+x3effdd7NmzB3V1dejp6YFSqYRCocCxY8eQn5+PrKws5Ofno7CwcIqo6eTn5+Pll1+WBu579+6ht7cXPT096O7uhtFonCFHFNTa2vpkgqbvLk0kEgiHw4hGoxgbG4PP54NGo8GVK1fw0UcfYdu2bdi9ezf27NmDbdu2SfLEn7dt24bKykrIZDKYzWbwPA+fzycFcWKX/POf/4yioiJkZWWhoKAARUVFKCwsnJWCggJJ0vbt26FSqeB0OqUun27wdrlcTyaI47gpYlIFRSIRqR4jrnuLO8G6urrQ0tKCq1ev4vLly7h16xZkMhkUCgV0Oh1MJtOUovr02MVoNEKr1eL48eNSdxMlzYYoavny5cjMzMTu3bthNBpnBJOpgSRFUd+voNHRUanbJRIJUBQFg8EAo9EIk8k0I3gzGAxSIV2n00mkLhOnLiOrVCqcPn0aBQUFyMrKQmFhIVauXJmWVFmpkvR6PWiaThtlUxSFe/fu/XCCxCx7Nmar28xWnvD5fPB4PKipqZEkFRUVobi4GMXFxY+VJUratWsXdDrdjJKs0+kESZI/rCBxQE9FmJafLUYWx3Gora1FYWGhJKmkpEQSlY6VK1dKkj788ENotVq4XK4pteonErRq1ap5C3pacmZrRWJLEiVlZ2dj5cqVKCkpmRVR1IoVK7Bs2TJ88MEH0Gg0UzZdkSSJ1tbW+N27d9c+FUHibCYWqqYLSlcCTbdgl07C9HpNuuTS7XbjwoULKCgoQHZ2NoqLi7Fq1aq0pMrKzc2VWpJOp5PSDJZlceHChcHr16//04IEbdiw4cfTBaVO9dMFia3FbrdLA2zqbvfZNhWkbrQUE83pyWZqrUehUKC3txdHjx5Fbm6uJKm0tFRiNlmipH379sFmsyEUCuHu3bu2o0ePLl+QnMe1oNRAMZ0glmWl5mu322G322Gz2SRSo1uR1HX01OXjdFO/uKVOp9Ph7NmzKCwsRE5ODkpKSrB69eopoqazatWqKZIaGxvZEydOlCxYTroWFI/HEQ6HEY/HZwhKHW/8fn/assJs5dB0xS2n0zmnZJvNBrPZjKqqKikfW7VqFdasWYPVq1fPipgEZ2VlITc3t6+lpeUfn0TQSY7jkEwmQVEUHjx4AJvNNqWLjYyMTBFksVigUCjmVQbt6+uT6O3tlVIDETGPE3O5dHR0dKCiokLKx0RJsyGKys/Px7Jly74tLS29olarX3hiQWazGXK5HBRFIR6PTxGUOlMFg8G0qwfTB9uFtKy5WpXFYkF1dTUKCgqQk5OD0tJSrF27dgpr1qyRHkVR+fn5WLp06berV6+uX7Ck6YKi0Si8Xi8EQZC6WDgcnrJbNBqNStPxdBmp9ZfpKw6zraen7ncWZx3xvaYLdTgcOHfunCRp9erVWLduHdatWzdDVqo0UVJpaenFBX01XBTE83zab/RNFyTGNxaLZday52zdLl1XS+1y/f39cDgcaeWkfpHO6XSirq4ORUVFkqT169dLotKxdu1a5Ofn48UXX0weOHBgw7wFbdmy5dmSkpIT0wWJM5m4vy+RSEwJ/rxe76zdZ64uJLYasQuldiOXyzWrHPE9KIqC3+/HlStXJElr1qzB+vXrH8vq1auxdOlSHDp06PC8BZWXly9JFZQaCwmCIEW/oqDHrY/Pdxl4trEqXddMN/uJ+xXD4TC+/PJLFBYW4uWXX8batWvxyiuvpGX9+vVYu3Ytli5dOn7o0KHtixY0PXUQ9/elDtjzWf6dTYwoJ90gPp/QQEwbxM2afr8fNTU1/zcvL+/b5cuXY+3atfjFL34h8corr0iPRUVFWLFihaqpqWn+0bQoiOO4tDlVqqD5tJp0ckQxC5UzfTAXBVEUJe07DAQCuHz5cn92dvbhl156KbRixQqsW7cOr7766hRRxcXFyM/PtzQ2NubOW85sglJnK/EPEQThieQ8bvqfjxzxGz2iIHFbXXNzs2nPnj3LADzzq1/96t2f//znkqRf/vKXePXVV7Fy5Urk5eWZFywnnaDp2XiqoPlk43PJmU9cNJsc8bnL5YLf70d7ezv1hz/8YaX4WQAs2bBhw5YXX3zRl52djfz8fKxYsQK5ubmmRclJFcTzvFQIm08XW2zG/rgAcraxZzo0TUMul/Offvrpa9P/XyAAS44cOVJaXFx8tbi4uKukpORsY2PjPy9KzuQb/l1BQcHh7u7uvyiVyvGhoaHxwcFBiYGBgXGtVjuu0WjG+/v7Jfr6+sb7+vrGe3t7x3t7e8d7enokuru7x7u7u8flcvm4XC4f7+rqGu/q6hrv7Owc7+zsHO/o6Bjv6OgYf/jwocSDBw/GHzx4MC6TycZlMtl4e3t7WmQy2V+uX78eOHjw4L8A+NFsn4vjuOc5jvtPAH68aDnisXHjxp++995769Kxfft2idnueVps3bp1TjZv3ry+rKwsB8CzT/zB/3b87Xgqx/8D2eEtvSevT9MAAAAASUVORK5CYII=)}.edit_frm .invoice_layout .btn.sdoc{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAfGUlEQVR4nO2bZ1Dcd5qgtTOeWfuuLs59mLoPV3W7V3VTV7d1O+u1JCQroICCZdleW/Z4LM3KPluSlaNHcpQsFCwhBAooECQBAkQQUdCIDA10Aw3ddM450xHweJi5fe4DdA8gULTn7sP8q55quvtPQz+87/t73x//njPnL8dfjv+vDuBHSqXy5yqV6n9qtdqXzp8/v/Szzz7b8MUXX2w8duzYGrFYnKDX638pl8v/1m63v/D/+vd9pqO4uPjHW7Zs+cnRo0efm43i4uIfHzx48F8fPnz4b86cOfPmnj17Mvbv39994sQJ54ULF6JZWVm/Lyws/ENJSckf8/Lyxq5du/btmTNnhj7//HPNrl27ylJSUg58/fXXL+7YseNnxcXFPz569OiPiouLf/w4HD169EePA/BXj+KpBG3YsOHfzZkz5x+ef/75ZZNYGuO5555b+rOf/ew3K1asyN29e7fhypUrowKB4F8GBwfR6/WYzWasVit2ux2nw4HdbsdisWAwGNBoNIjFYu7cufOH5ORk/zvvvNP4i1/84sjzzz+/7IUXXljw3HPPPQ4JMV544YWEF154Yf5PfvKTeTMw9xG89POf//x/CIXCf/NEgjZv3vzvX3rppdPZ2dnOgoICR0FBgaOwsNCRl5fnOHHihOOjjz5yHj16NFpXV/cvSqUSq9WK0+HAZrViMpvRGwyotDpkKg1SlQaFWotWp8NgNGKxWLDbbDjsdgwGAz09PeTk5Px+x44dnn379tkzMzNtBQUF9tu3b9sLCgoeoLCw0FZYWGgrKiqy3S7It/32xEf25HOHbUVFRbaioqL4c4/DnTt3bCkpKfLCwsLlTxxBSUlJVx0OB8PDw4yMjOByubhz5w4XLlygsrIStVqNw27HZrWi0ekRSRXUiga43dpDTqOYzEYxVxvEXLkv5nqDmKxGMbeaxVR0SmiXyJAqVZhMJuw2GxaLBZFIRE5ODhkZGfT398d/7vDw8IyMjIwQCPk4fvPXHLi8gua+EqLDEUZHRxkZGYkzOjo6K99++y0ikShSV1e3/okFrVy58orNZiMajaLVarl69SrXr19HKpXGU0aqVFErGiC3pZfL9SJS60Sci1HbPcHkr7tJrRORLhCR1dRDmVBC94AcrU6HzWbFYDBQW1tLamoqDQ0NBINBIpEI4XCYSCQy5etoJIpvyEPyrU1crT3A0ZsbKLp/Dq/fxXB0mGg0SjQanVVwTHJnZ+fTCVqxYkWGzWZDIpFw/vx5SkpK0Ov1WK1WFGoN93uk3Gjp5cL9HtLv93BxEhdmYaZzrjb2Ut7ZT49MgcFoxGq10tnZSVpaGsXFxXi93riYyaKi0QlBNzfSNJhPn1nAydsbybj7CRanjuHh8eiLDj8oKSbuqQW99957/zYpKSmjsbGRM2fOUFtbi9lsxmwyIZbKKWyXkNHYS0ZTH9dbJGS19pM9iZzWfnLaJjHt+RhZrf1cax5/rZyWPhp6pKjVGiwWC/39/Vy4cIG8vDz8fv8DkTRZUKMsF3NwEJm9hYuVuzmd9z4qYy8jMUmzRNLo6OjTC5o7d+6V5ORkBAJBfAVqlwyS2ybhSpOE7LYBbncOUtCloLBbQbFIyd0eFVV9au71axAMaKmXahEMaKkb0FIj0VDRq6asR0WxSElht4LCLgUFXXJyhTKut/ST2SyhRjSAXKnCYrEwMDBASkoK5eXlf0qtidSJCTp+cyMNslto/SJUXiFqTze5Tcf4MvstumS1cTnTb2P16akEbdq06b+uWbOmtbS0FLPZjE6vp6lPxs22fjJbB7jdJedur5qKPg01/Voa5QY6VCa6NGZEGjMirWVWujRmOlQmWhRGBDI9Vf16KiQ6yno15ArlZLdJqeiWIVOosVisiERiznxzhtraOoxGEyajGZPRhMlkRq1V8sX1t7kvu4Xa14XU1Yjc3YrG102F+CKfZ71OScMlnG4HI8MjUyLpqQUBz33wwQenr169+kedTofRaByPnPYBctqllPaoqJMZuD9opENtRmKwITXa4wwY7fQb7PTqbYi0Vjo1FjrUZjrUZjo1Frq1Fnp0VvoNdjqVCvKbCrhyL5uMmmwuVmdxujSDE3cucaX8MiWC61S13ORCzlfs+vQ9Lt36mhslKeRMkHnnJDvOLqZBdguVV0i/sx6Jo45+Zy1KbxtNilw+z36dq2Wf4vbZGBkZjct5akFZWVkLv/jiC5dcLo/XnKJOGTc7ZNztVdOsNNOusdBvdKC0ulDZxlFaXcjMTnoNdrq0NtrVFlonaFGNE7vfprbQqbVzsz6bAxkrOVe6lXOl2zhXuo2U0m2klGwlpWQrqaXbOF+6jdTSbZwp+pCU4i2klmzlXMnWidstpJdvp9dci9zTSp/jHr32KsS2ckS2MqSuOroMpZwu2sj5Ozsx2lVT2oAnFmQ2m5/fu3dvbnV1NWazGZlCRYVokDzhIHd71bRrrIgNdpQ2N3qnF4PTi97pQWVz0292IjLY6dI9Ht16B1fvXeJS5S7Uni4U7vaH4+lA4XnwcaVHiMrbSb9TgNhWgchaRpe1mE5LIR3mfMT2UnosFVyq3sHxm+8i1XQ8vaC8vLxXjh8/HlKr1eh0Opolcgq75dwRKcejxuJC6/Ri9fixevyY3D6Udg/9Zhd9JucTITG7yaq7TEb1XhSeNnrtNU9Nj70asa2cbmspnZYiOsy3aTfl0mq6QYsxiw7zLXptd7nV8imfZa6nqbeEaDTCt6PfPr4gl8v1r/bs2VNeV1eH2WymX66kskdJsVhJ/aABqdWD3uXH4Qvg8Acwe4ZQ2r3IrJ6nYtDm40b9FTKq9yJ3tyK2VT01IlsF3dYShJYiOsz5tJlu0mS4Rr3uAjXqs9xVfEXx4GHuac5Q0PUFn2atpbgxjWDIT1dX9+MJun379t9/9dVXTpVKhVarpaVfQWWfmkqJhl6TE717CNdQCE8ghM0XRO30o3T4nhqVc4hbDVfJqN7LoKsFkbXiKSmny1qK0FxEmymXBv1VKpQnKZB+wg3Jx1zv+WeuiN7lUvdbXOp+iyLZfu72H+er3PVcqzhMraAiUltb++ojBZ08eXJ3ZmbmH00mEzKFEkG/mup+LfflRjSuIVyBMP5QBGcghMETQOsOoHsG9J4Q+Y3Xyajei8zVRLf17lPRZSmjw1zIfd1VyhUnuT1wiBt928ns+d9cFW8iQ/QrLna9SXrna6QJ13Fe+Ao3+j6kcjCZ0yW/Yl/K2m9Tc4788yPT68MPPyxqamrCaDTSI1NwX6ajZkBHt96BzR8iEI7iDUUw+0IYvcFnxuQLU9A8IcjZSKel9KloMxVQo06nSPYFeQMHuCnZRXbvVq73bOaK6Ndc6n6L9M7XOC9cy7mOJFLaV3C2fTlXxO9QoTjGhZoP+U3yL/rTS3Ysn3V/KC8v77/t3r3bOjAwgFarpXNQTbPCgGDQgNLhwxeOEoxGcQTCWPzPwFAYa5wIBc3XuVy9B6mzAaG5+IlpMeZRpTpP8eBRCqRHyOvfz42+HWT1fsQ18W+4LHqHC11vcF74CikdKznTnsg3bYsnWMKFrvWUyT/jZttBtpz7e2t6ycfvA3/9gKAzZ84sOnXqVESj0aBQqWiX62hRmmhQmjD5goSiI/jCUeyByNMTjGAZCqNzB5BaPYiNLjJqLnC5eg/9jno6TEVPRIshl2pVOmXyk9yRfUWB9DC5/fvi6XVFvJFL3RtI71xPascqzrYncrr1ZU62zudk63xOtS7km7bFpHeu457mFHntR3j367/x3Kr7OmEmQW9eunRp1GgwIFMoESoNtKnMtGmsOAJhQsMjuELDOEPRJ8IViuIKDWMPRNC4hug2OGhWmWlUmGhQmEmvOD8hSECbsWAc00OYeL7ZkEu16gLlirOUyk9SJPuS29Lfcqt/Lzl9H3O95wOuiH/Nxe43SetcR0rHSr5pW8zJ1vkcb36R483/QHLLS5xtT+S2dBfFPV9y4Ori6P4LK05bw4P/4QFBZ8+e3X3t2rUxk8mEVK5ErDHRrjbTpbfjDEYYigzjDj8ZnvAwzmAUnTtAj9FJq9pCs9JMs2qcFrWVi5VpXK7eg8RRR6sx/7FoNtyiRn2RckXKhKAT0wRt43rP++OCut4cT6/2FZxuW8SJlrkca/pfHG36O860L6FItp/slr18nPqibV964hYz5udnrEFpaWmnsrOzMRmNDMiV9OksdGistGutDFjcGL1BHMEInvAw3sgE4WlMetwVimLwBOgzu+jQ2uLjRZvGGqddY+NSVTqXq/fQZ6+l2ZD7UFoMuTTpb1CjvkS5IoUKxblZBH38J0HTIuhEyzyON/+S88KVFMsOkVazme3fLPruWmHy7odu4KelpaXn5ORg0OsZUKjoN9jo1NkQasfp1NnpNblQ2L2YfEEcgQjOYCyFojiDURyBCCZfEJXTR7/FTZfOTof2T68xnU6dg4zq8RrUa6uhSX/zodTrMqlWpVOhSBkXpDxHuSIlXoNuT9SgnCk16C3SO1/lXMcqzrQv5XTry1wVv03xwGGSC9/gdN4HFNzNeXQfdO7cuaOZmZn/otfrkSpUyIw2xAY73fo/0TVxKzI46DE6kZhdDFjdDFjcSMwuek1ORAbHlHMfhsjg5OpEke6xVdOgz5lCoz6HBl029dpM7qkvU6lMnUirqZTJT3FnYhXLja1iPR9yVbyJy93vcKHrdVKFa0gVriZv4GMKew7zac5qrpT9FqNFQ2trW6SmpubhnfTp06e3pqam/l6v1yFTKBk02pCYnPQYH454gkedNxO9JjfX713kctUexLYqGnTZ3Ndlc1+XRb0ukzrtVWrUl6hUnqdCeW5W7iq+oUR+nELZZ5P6oC1c79lMhuhdLna9SYbobUrln3KzbT+Hr6/mzv107A4rfp+fpqamRwv65ptvXv/iiy9GlUolgwolcqMVmcWFxPzD0W/2kFl7kYuVO5E6G8ZnKmtFvDvutJTSaS6ZiiVGrFEcv99myqNMnkz+wCFuSfaQ0/fxRCe9kRt9W6hWneJS7TY+y3yNRlEpbo8br9eL1+t9PEEnT55M2L59e7i/vx+FUonCYEZpczFgcSP9gZBZveQ33WTvpeUk528kOX9TnBP5mzh5e/w2dv9E/iaS8zaSnPsex27+iqM33iE57z1OF/6GVm0B1epUCqSHyevfz03JTrJ7t3Jn8DD3FKmcKd1I8s1NSJQd+Hw+PB4Pbrcbj8dDY2PjowUJhcL/snnzZnVraysqlQqFVo/W7kZu8zL4AyG3++gxGKjsrqJMWBbnrrCMhp5yWvoqqOgso0xYSrWwhLuthRTXXie3LJ19n2/myzO7yb17np0pS6mWXOKeJp1C6WfkDxwir38/1epvqBpI5cubr5NWuAedSYnP54/LeSJBwE/fe++97Ly8PDQaNXKVGr3dhdrhQ+nwo/oBUDp8qBx+1M4Aalcwjs0XIhyO4BkKo3EF0boCmF0+1EYrvb19CDuEfPXlUVqaWzGZDXyV+Q7VksvUaS9xR/YlxYNf0aC/SonoJJ9mriPv3hmMJh0+r2+KHJfLhdvtpqGh4dGC5syZM+f8+fMfHDly5Du5XI5coURnsaF3+dC6h/5sWL0BAsEggWAQs3cIjXsIo8uLweFGrtbQ0dFBWVkZR48exefzMRT0cvzGe1RLMhDorlClOkebMZ8bjUf4PPMN7ncVxevNdDlPLKiuru6/b9682dzU1IRCoUCtM2B2ejB4Aui9wR8UgzeI3T8uJhQK4R4af8zgGcLi8qCz2unt66OxsZGzZ8+Sk5NDNBrF43Xydc6vqZFcodNaTLu+kPSK7STf3ESvvBWfzz+jHKfTidPpxOVycf/+/ccTBPx006ZN2RcvXiQWRUabHYt3CJMv9INh9oVwDoUIBEOEQiH8gRBW//hzFo8Pi9ONUqOlubmZiooK9uzZg0gkIhwO43TbOZbzLnXSTNq1xZws2MiFO3vRGAfxT9Sb2eQ4HA6cTif19fWPJ2jOnDlzzp49m7Bx40Zna2srcrkctU6PzeXB6g9N2qb4/nAMhfAFggSDIUKhIIFgCGdg/DmbL4Dd7cVgtdPT04NAICAtLY1z587h8/kIBII43TaO5bzLlXv7+OrGW9yuO4vNYY6vVA+TE+OJBAE/fuedd86fOnUKsVjMoFyOwWzF4fXjCERwBKPfC65gBG8wPBE1wbgcTzA8fk4gjMPjxeryMKhQUl9fT2lpKYcOHaKvr49QKEQgEMDptvFV1tvsTV9KfVcBHo8bn88XT6uYoJnk2O12HA4HAoHg8QXNmTNnTkNDw9+99dZb+vz8fPr6+lAqVVhsNlz+AO7Q1IndF4rgC4XxhCK4Q9HZp/tQFE9ofEdyKBQmGPqTmGAwSCAUwheKxM91en043B7UWh1NjY2Ul5fzySef8NFHHyGVSnE4HNhsNkxmIxWN2bSKqrFYLJjNZkwmEyaTCaPRGMdqtT4gx2azYbfbn1zQnDlz5rz//vu7t2zZMnLv3j0kEglqjQab04l7KIgvMoIvOoovMsJQOEowFCIYChEIhQiEwgyFwgyFIgyFI+O3oTCBiXOCoSCh4J8ITnyPPzKMLzo6vhPg8+N0e9AbTbS3t1NZWcn58+dJSkpi/vz5bN++HYFAQHd3N52dnXR1dtHV2YVQKEQoFNLR0fEACoUiLigm55kEvfvuu//pl7/8ZcWRI0eoqqqip6cHjVaLze7AMxRgKDpKYPjbcaIjBCNRguFYZIQIBscjIzSN4ISUYChEMBwhGBmOv44/Mozb68PpcqM3mhAKhVRWVpKTk8Nbb73FwoULSUhIYN68eezevRuJRILFYnkgYgwGAwaDAb1ej16vR6fTYTabsdvtU+RYrVZsNht1dXVPLmjz5s3PL1iwIH3//v0cOXKE8vJyRCIRKrUaq9WK1z9EIDJMePR3UxkZJTw8Qig6TCgSJRSJTCI6TnRk/LyJ7wmNfIs/GMbt9eJ0udBotbS0tFBeXk5mZibvvvsuixYtipOQkMD8+fM5dOgQ3d3dKBQKBgcH48hkMmQyGVKpFKlUysDAADqd7gE5MZ5a0Jo1a9K6u7s5dOgQn3/+OWVlZXR0dDA4OIjJZMbpcuEPBAmPjDL87XcM/+730/hu/PEY056Pjv6OYGQYr8+Py+3GZncgVyhoaGjg7t27XLlyhV/96lcsWbKExYsXT2HBggXMmzePrVu3IhAIEIlEdHd309XVRVdXF52dnVNQKBRTxFgsFiwWy7MJWrVqVZrD4UCr1ZKcnMz+/fvJzs6mrq4OsViMWq3GYrHgdnvwDw0Rjg4zPPo7Rn73e779boxvfz+V0e/GGP72OyIjowRCYbxeH263B7vdgVqjQSgUUlVVRUlJCampqbz55pssWbJkRmKS5s+fz/79+5FKpVit1hmLtMFgwGw2xwXF5JjNZiwWy7MJil2jaLfbuXTpEh988AHnzp3j7t27NDY2IhaLUSqV8Rx3uz34fD78Q0PxkSEQDDIUCODz+yekuHE4HFgsFlQqFV1dXdTU1FBWVkZ2djZ79uxhzZo1LF26dFZiohYuXMi8efPYuXMnAoGArq6Zi/Xg4OADcmLU1tY+taDzNpstfmWXz+ejoqKCffv2cejQITIyMiguLubevXu0trbS29uLQqlEo9HEr5WOrRQWiwW9Xo9Gq0WpVCISiWhoaKCyspLS0lJu377NqVOn+OCDD1i+fDnz589n6dKlJCYmzshkWQsXLmTu3Lls27aN1tbWeB2K1SCpVIpGo4kLiokxmUyYzWbu3bv35IJ27dr110lJSakxQZOvDTQajeTk5LB161b27NlDamoqhYWFlJaWUllZSW1tLQ0NDTQ1NdHS0kJLSwvNzc0IBAKqq6upqKigrKyMkpISrl+/zmeffcb777/PJ598QlVVFRcvXmTp0qUkJCSwdOlSli1bFmc2YQsXLmT+/PkcPHhwfMXVaFCpVKhUKpRKJXq9/gE5RqMRk8n0/QuKRqMMDQ0hlUrJysrit7/9LTt27ODAgQOcOnWKtLQ0bt68SWFhIYWFhRQVFVFcXEx+fj4ZGRmkpqZy7NgxPv74Y7Zt28bBgwfJycmhtbUVkUiEUCjkzJkzJCYmkpCQQGJiIsuXL58iajqJiYm8/PLL8cJdXV1NW1sbra2ttLS0IJVKH5ATE1RTU/NsgqZfXRqJRPD7/QQCAUZHR3G5XPT29pKfn8/hw4fZuXMnBw4c4ODBg+zcuTMuL3Z/586dpKamIhAIkMvlOBwOXC5XvImLpeSNGzdYsWIFCQkJLFu2jBUrVrB8+fJZWbZsWVzSrl27EIvF6HS6eMrPVLyNRuOzCbLb7VPETBY0NDTE8PAwkUgkfkW8Xq9HLBbT1NREZWUlt2/fJjc3l9LSUgQCAUKhEIlEgkwmQ6FQIJfL433L9LrR19fH2bNn4+kWkzQbMVGLFi1i/vz5HDhwAKlU+kAzObmRNBgMP6ygkZGReNpFIhEMBgMDAwNIpVJkMtkDzdvAwAD9/f309/cjkUji9PX1xent7Y0jFotJT09n2bJlJCQksHz5clauXDkjk2VNltTf34/ZbJ6xyzYYDFRXV//5BMWm7NkYGhrC7/fj9/vx+XzxyXv69D15189ms5GVlRWXtGLFCpKSkkhKSnqorJik/fv3I5FIMJlMD4wger3+zysoVtAnE5o2nz2NLLvdTnZ2NsuXL49LWrVqVVzUTKxcuTIuad++ffT19WE0GuNynlnQ6tWrH1vQ9yVntiiKRVJM0oIFC1i5ciWrVq2alZioxYsXM2/ePPbu3Utvby96vR6tVotWq0Wv11NTUxOuqqpa970Iiq1mQ0NDMwqKvfnJAmISHpVK0/drZhouTSYT165dY9myZSxYsICkpCRWr149I5NlLVmyJB5JEokkPmZYrVauXbvWVVRU9LdPJGjDhg0/nS5o8lI/XVAsWjQaTbzA9vT0xBGLxXFEIlGc7u7uOLFBc/qwGRsdYrS1tXHy5EmWLFkSl7RmzZo4s8mKSfrkk09Qq9X4fD6qqqrUJ0+eXPREch4WQZMbxZkEWa3WePhqNBo0Gg1qtTrO5O42hkKhiC/5crl81m2L2NbFwMAAEomEixcvsnz5chYuXMiqVatYu3btFFHTWb169RRJxcXF1pSUlFVPLGemCAqHw/j9fsLh8AOCJtcbt9s947bC5C72UZtbOp3ukZLVajVyuZy0tLT4PLZ69WpeeeUV1q5dOyuxITghIYElS5a0V1ZW/udnEXTObrcTjUYxGAzU19ejVqunpNjw8PAUQQqFAqFQ+EBqzLQN2t7eHqetrS0+GsSIzXGxWW4mGhoaOHXqVHwei0majZioxMRE5s2b98c1a9bk9/T0/PyZBcnlcpqbmzEYDITD4SmCJq9UXq93xv8eTC+2TxJZj4oqhUJBRkYGy5YtY+HChaxZs4Z169ZN4ZVXXonfxkQlJiYyd+7cP65duzbviSVNFxQIBHA6nYRCoXiK+f1+otHolCU8thxPlzF5/2X6fxwmp9hMQrRabXzVme0/F1qtlitXrsQlrV27lldffZVXX331AVmTpcUkrVmz5voTfTQ8JsjhcEz5hF+M6YJi/Y1CoZh123O2tJsp1SanXEdHB1qtdkY5sUgzGAzodDpycnJYsWJFXNL69evjomZi3bp1JCYm8uKLL0aPHTu24bEFbdmy5SerVq1KmS4otpL5fL7450gnN39Op3PW9HlUCsWiJpZCk9PIaDTOKif2GgaDAbfbTX5+flzSK6+8wvr16x/K2rVrmTt3LsePH09+bEFHjx59brKgyb1QKBSKd78xQdObw8eZsx63Vs2UmjOtfnq9Pv6Hu3XrFsuXL+fll19m3bp1vPbaazOyfv161q1bx9y5c8eOHz++66kFTR8dvF4vPp9vSsF+lJjJHfNsXfNMRfxxWoPY2OD1ehkaGsLtdpOVlfV/li5d+sdFixaxbt06Xn/99TivvfZa/HbFihUsXrxYXF5e/vjddEyQ3W6fcaaaLOhxomYmOTExTypnejGPCTIYDHg8nvjPzs3N7ViwYEHySy+95Fu8eDGvvvoqb7zxxhRRSUlJJCYmKoqLi5c8tpzZBE1erWK/SCgUeiY5D1v+H0eOVqudIih2WV1FRYXs4MGD84Afv/3229v+8R//MS7pn/7pn3jjjTdYuXIlS5culT+xnJkETZ/GJwt6nGn8UXIepy+aTU7sa6PRiNvtpq6uzvDpp5+ujL0X4LkNGzZsefHFF10LFiwgMTGRxYsXs2TJEtlTyZksyOFwxDfCHifFnnZif1gDOVvtmY7ZbKa5udnx5Zdfvjn9YwXAcydOnFiTlJR0OykpqWnVqlUXi4uL//6p5Ey84F8tW7YsuaWl5Q8ikWisu7t7rKurK05nZ+dYX1/fWG9v71hHR0ec9vb2sfb29rG2traxtra2sdbW1jgtLS1jLS0tY83NzWPNzc1jTU1NY01NTWONjY1jjY2NYw0NDWMNDQ1j9+/fj1NfXz9WX18/JhAIxgQCwVhdXd2MCASCPxQVFXm+/vrr3wA/mu192e32F+x2+38EfvrUcmLH5s2bf7F9+/ZXZ2LXrl1xZjvn+2Lr1q2P5KOPPlp/5MiRhcBPnvmN/+X4y/G9HP8XQHLbNqTL5PYAAAAASUVORK5CYII=)}.edit_frm .invoice_layout.freeze::before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(50,50,50,.3);user-select:none;cursor:wait;z-index:10;pointer-events:none}.edit_frm .invoice_layout.freeze::before tr:hover>td.aux>.axf{display:inline-flex}.edit_frm .invoice_layout.freeze::after{content:"";position:absolute;top:50%;left:50%;width:3rem;height:3rem;margin:-1.5rem 0 0 -1.5rem;border:.4rem solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;z-index:11;pointer-events:none;animation:.8s linear infinite fis_spin}.edit_frm .invoice_layout .dvalidation{margin:0 0 1rem}.edit_frm .invoice_layout .dvalidation.hidden{display:none}.edit_frm .invoice_layout .dvalidation .dvmsg{padding:.4rem .7rem;margin:.25rem 0;border-radius:.2rem;font-size:.9rem;border-left:4px solid #999;background:#f5f5f5}.edit_frm .invoice_layout .dvalidation .dvmsg.error{border-left-color:#c0392b;background:#fdecea;color:#922}.edit_frm .invoice_layout .dvalidation .dvmsg.warning{border-left-color:#e0a800;background:#fff8e1;color:#7a5c00}.edit_frm .invoice_layout .dvalidation .dvmsg.info{border-left-color:#3498db;background:#eaf4fb;color:#1c5a82}.dhist .invtbl{width:100%}.dhist td,.dhist th{padding:.3rem .6rem;text-align:left;vertical-align:top}.dhist tbody tr:nth-child(even){background:#fafafa}.modal-body .lstfrm{display:block;position:relative}.modal-body .lstfrm .li{display:inline-block;background-color:#fff;border:1px solid #e0e0e0;padding:1.7rem .5rem .5rem;border-radius:.2rem;cursor:pointer;position:relative;margin:0 .5rem .5rem 0;box-shadow:1px 1px 3px rgba(50,50,50,.1)}.modal-body .lstfrm .li .lbl{position:absolute;background:#727272;color:#fff;font-size:.8rem;left:0;top:0;right:0;padding:.2rem;border-top-left-radius:inherit;border-top-right-radius:inherit} \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fis.js b/Fuchs/wwwroot/web/fis.js index c78be9d..c2802c0 100644 --- a/Fuchs/wwwroot/web/fis.js +++ b/Fuchs/wwwroot/web/fis.js @@ -814,7 +814,7 @@ $ocms.postXT = function (options) { if ((typeof options.contentType === 'boolean' ? options.contentType === false : false) === true) { ajo.contentType = false; /* needed for file uploads */ } - $.ajax(ajo); + return $.ajax(ajo); }; $ocms.cex_timer = function () { if (!$ocms.cexi) { @@ -897,16 +897,16 @@ $ocms.vbl_send = function (ev) { } else { val = null; } - } else if (type === 'number' && typedvalues === true ) { - let v; + } else if ((type === 'number' || dtf[0] === 'float' || dtf[0] === 'integer') && typedvalues === true) { + let v, raw = String(val).replace(',', '.'); if (dtf[0] === 'integer') { - v = parseInt(val); + v = parseInt(raw); } else { - v = parseFloat(val); + v = parseFloat(raw); } val = isNaN(v) ? val : v; } - if (req === true && ((val || '') === '' || (val.match(pattern) === null))) { + if (req === true && ((val === '' || val === null || typeof val === 'undefined') || (typeof val === 'string' && val.match(pattern) === null))) { if (cat === false && bool(checkvalidity, false) === true) { e.setCustomValidity(t.nza('ocms-nvnote', $ocms.t.inv || 'Invalid field')); } val = null; } else if (cat=== false && bool(checkvalidity, false) === true) { @@ -2220,6 +2220,8 @@ class NumArray extends Array { break; case 'number': f.pattern = ne(f.pattern, '[-+]?[0-9]*[.,]?[0-9]*'); + inp.attr('type', 'text'); + inp.attr('inputmode', 'decimal'); inp.attr('step', f.precision || 'any'); inp.attr('data-format', 'float'); break; @@ -2533,7 +2535,8 @@ if (!Element.prototype.closest) { this._dragging = false; this._dragHandleClass = this._options.dragHandleClass || ''; this._parentident = this._options.parentident || ''; - this._swapdone = typeof this._options.swapdone === "function" ? this._options._swapdone : null; + this._swapdone = typeof this._options.swapdone === "function" ? this._options.swapdone : null; + this._onend = typeof this._options.onend === "function" ? this._options.onend : null; this._container.setAttribute("data-is-sortable", 1); this._container.classList.add("sortable"); @@ -2667,7 +2670,15 @@ if (!Element.prototype.closest) { _onPress: function (e) { if (!e) { return; } function op(tgt) { - if (tgt && tgt.parentNode === this._container && (this._dragHandleClass === '' || e.target.className.indexOf(this._dragHandleClass) > -1) && tgt.className.indexOf("nosort") < 0) { + // Match the drag handle by real CSS class token, not substring. The old + // e.target.className.indexOf('ico') matched every Bootstrap "glyph[ico]n" icon, + // so clicking any icon button (edit/delete/set-price…) inside a sortable row + // started a zero-distance drag that swallowed the button's own click. Using + // closest('.') restricts dragging to the dedicated handle element (e.g. + // the ".ico" move button) and its descendants only. + var isHandle = this._dragHandleClass === '' || + (e.target.closest && e.target.closest('.' + this._dragHandleClass) != null); + if (tgt && tgt.parentNode === this._container && isHandle && tgt.className.indexOf("nosort") < 0) { e.preventDefault(); this._dragging = true; this._click = getPoint(e); @@ -2683,10 +2694,17 @@ if (!Element.prototype.closest) { } }, - // on item release/drop + // on item release/drop _onRelease: function (e) { + // Was THIS list mid-drag? (mouseup fires on every instance's window listener.) + var wasDragging = this._dragging === true && this._clickItem !== null; this._dragging = false; this._trashDragItem(); + // Fire a single "drag finished" callback so callers can commit the new order once, + // reliably, on drop — rather than relying on the per-hover _swapdone during the drag. + if (wasDragging && typeof this._onend === 'function') { + this._onend(); + } }, // on item drag/move @@ -2824,6 +2842,7 @@ $.extend($t, { , m_rep: 'Berichte' , m_todo: 'ToDos' , m_bcd: 'BankBuchungen' + , m_admin: 'Administration' , rsp: 'Passwort ändern' , pnm: 'Die Passwörter stimmen nicht überein' , cps: 'Das neue Passwort wurde gespeichert.' @@ -3129,6 +3148,68 @@ $fis.notifications = { } }; +/* Live draft-editing client (ADR 0006/0007). Separate SignalR connection to the + dedicated /draftpreview hub; the server signals the *one* browser editing a draft + (group = session token) to re-fetch (draftReady), warns before idle expiry + (draftExpiring), and tells it to close on eviction (draftClosed). The editor + (fis.inv_shared.js) registers the open draft via $fis.draft.bind(token, {...}). */ +$fis.draft = { + connection: null, + active: null, /* { token, onReady(version), onExpiring(secondsLeft), onClosed(reason) } */ + init: function () { + if (typeof signalR === 'undefined' || this.connection !== null || !$ocms.auth.useraccount_id) { + return; + } + this.connection = new signalR.HubConnectionBuilder() + .withUrl('/draftpreview') + .withAutomaticReconnect() + .build(); + this.connection.on('draftReady', (p) => this._dispatch('onReady', p, (p) => p.version)); + this.connection.on('draftExpiring', (p) => this._dispatch('onExpiring', p, (p) => p.secondsLeft)); + this.connection.on('draftClosed', (p) => this._dispatch('onClosed', p, (p) => p.reason)); + /* Re-join the active draft's group after a (re)connect — group membership is + per-connection and is lost when the socket drops. */ + this.connection.onreconnected(() => { if (this.active) { this._invoke('JoinDraft', this.active.token); } }); + this.connection.onclose(() => { + console.warn('Draft connection closed; retrying in 5s.'); + this.connection = null; + setTimeout(() => { this.init(); if (this.active) { this.bind(this.active.token, this.active); } }, 5000); + }); + this.start(); + }, + start: function () { + this.connection.start() + .then(() => { if (this.active) { this._invoke('JoinDraft', this.active.token); } }) + .catch((err) => { + console.warn('Draft connection failed to start; retrying in 5s.', err); + this.connection = null; + setTimeout(() => this.init(), 5000); + }); + }, + /* Registers the currently open draft and joins its signal group. handlers: + { onReady, onExpiring, onClosed }. */ + bind: function (token, handlers) { + if (!token) { return; } + this.active = $.extend({ token: token }, handlers || {}); + if (this.connection === null) { this.init(); } + this._invoke('JoinDraft', token); + }, + /* Unregisters + leaves the group (editor closed). */ + release: function (token) { + if (this.active && (!token || this.active.token === token)) { this.active = null; } + this._invoke('LeaveDraft', token); + }, + _invoke: function (method, token) { + if (!token || !this.connection || this.connection.state !== 'Connected') { return; } + this.connection.invoke(method, token).catch((err) => console.warn('Draft ' + method + ' failed', err)); + }, + _dispatch: function (handler, payload, argOf) { + payload = payload || {}; + if (!this.active || this.active.token !== payload.token) { return; } + if (typeof this.active[handler] === 'function') { this.active[handler](argOf(payload)); } + } +}; + (function () { Array.prototype.push.apply($ocms.ocmsmenu,[ { lbl: $t.m_inv, id: 'm_inv', fnc: 'init:inv', ico: 'glyphicon glyphicon-list-alt'} @@ -3141,7 +3222,26 @@ $fis.notifications = { //, { lbl: $t.m_efa, id: 'm_efa', fnc: 'init:efa' } ]); })(); + +/* The Administration module is restricted to users whose fds_sys authorization exceeds 4. + The button is added — and only then does clicking it load the module script — after the + asynchronous auth check resolves, so lower-privileged users never see it or fetch the code. + Called from fis_main_go.js once the DOM (and the initial main menu) is ready. */ +$fis.addAdminMenuIfAuthorized = function () { + if ($('#m_admin').length > 0) { return; } // idempotent + $fis.getAuth('fds_sys').then(function (auth) { + if ((auth || -1) > 4 && $('#m_admin').length === 0) { + $ocms.ocmsmenu.push({ lbl: $t.m_admin, id: 'm_admin', fnc: 'init:admin', ico: 'glyphicon glyphicon-cog' }); + /* Re-render the main menu cleanly: drop the previously generated menu list + (never the .nav-right settings dropdown) and rebuild it from the array. */ + $('#mainmenu').children('ul:not(.nav-right)').remove(); + $('#mainmenu').ocmsmenu($ocms.ocmsmenu); + } + }).catch(function () { /* auth lookup failed → leave the menu unchanged */ }); +}; $(document).ready(function () { $fis.notifications.init(); + $fis.draft.init(); $fis.ov(); + $fis.addAdminMenuIfAuthorized(); }); diff --git a/Fuchs/wwwroot/web/fis.min.js b/Fuchs/wwwroot/web/fis.min.js index 7999308..b7b0edc 100644 --- a/Fuchs/wwwroot/web/fis.min.js +++ b/Fuchs/wwwroot/web/fis.min.js @@ -1,4 +1,4 @@ -var t,e,$t={lng:"de-DE",dn:["So","Mo","Di","Mi","Do","Fr","Sa"],mn:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],ma:["Jan","Feb","Mrz","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],datepattern:"(0[1-9]|1[0-9]|2[0-9]|3[01]).(0[1-9]|1[012]).[0-9]{4}",datetimepattern:"(0[1-9]|1[0-9]|2[0-9]|3[01]).(0[1-9]|1[012]).[0-9]{4}\\s([0-5][0-9]):([0-5][0-9])",dateplaceholder:"dd.MM.yyyy",datetimeplaceholder:"dd.MM.yyyy HH:mm",dateformat:"dd.MM.yyyy",datetimeformat:"dd.MM.yyyy HH:mm",f1:"Der Server hat einen Fehler gemeldet: \n",f2:"Bitte versuchen Sie es erneut.",m0:"Diese Internet-Seite benötigt einen html5-kompatiblen Browser.",m0b:"Unterstützt werden bspw: Internet Explorer ab Version 10, Firefox ab Version 31, Chrome ab Version 31, Safari ab Version 7, Opera ab Version 27",m1:"Dieser Datensatz ist momentan von jemand anderem zur Bearbeitung gesperrt.",m2:"Diese Funktion ist zur Zeit nicht verfügbar",t1:"Eingabe erforderlich.",t2:"Eingabe ist nicht erforderlich.",true:"Ja",false:"Nein",alert:"Hinweis",confirm:"Bestätigen",open:"Öffnen","not implemented":"Diese Funktion in zur Zeit noch nicht verfügbar.",l0:"Anmeldung",l1:"Email / Anmeldename",l2:"Email-Adresse / Anmeldename",l3:"Passwort",l4:"Benutzer",l5:"Wird vom System ermittelt...",l6:"Anmelden",l7:"Passwort vergessen?",l7a:'Die "Passwort vergessen"-Funktion läuft in zwei Schritten ab:\n \nIm ersten Schritt wird eine SMS mit einem Code an die hinterlegte Mobilfunk-Nummer versandt.\nIm zweiten Schritt geben Sie bitte diesen Code in das Formular ein und übermitteln es erneut.\n \nIn beiden Schritten wird aus Sicherheitsgründen kein Fehler angezeigt und auch dann ein erfolgreicher Versand bestätigt, wenn die Kombination aus Email-Adresse und Nachname nicht gefunden wurde und/oder der code falsch ist.',l8:"Keinen Account?",l9:"Anmeldenamen der Email-Adresse wurde nicht erkannt.",l10:"Nachname",l11:"Email-Adresse",l12:"Passwort zusenden",l13:"Das Passwort wurde erfolgreich verschickt",l14:"Das Passwort konnte nicht verschickt werden",l15:"Sie sind nicht berechtigt, diese Funktion auszuführen.",l16:"Sie müssen zunächst einen Account angeben.",l17:"Die Kombination aus Anmeldenamen und Passwort konnte nicht bestätigt werden.",l18:"Es gibt ein Problem mit dem Formular.\nEs kann momentan nicht verarbeitet und versendet werden.",name:"Name",submit:"Senden",cancel:"Abbrechen",noop:"Diese Funktion is noch nicht verfügar."};t=self,e=()=>(()=>{var t={d:(e,n)=>{for(var o in n)t.o(n,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:n[o]})}};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),t.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),t.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"t",{value:!0})};var e,n={};t.r(n),t.d(n,{AbortError:()=>r,DefaultHttpClient:()=>D,HttpClient:()=>h,HttpError:()=>o,HttpResponse:()=>d,HttpTransportType:()=>U,HubConnection:()=>O,HubConnectionBuilder:()=>tt,HubConnectionState:()=>j,JsonHubProtocol:()=>K,LogLevel:()=>e,MessageType:()=>N,NullLogger:()=>p,Subject:()=>q,TimeoutError:()=>i,TransferFormat:()=>B,VERSION:()=>f});class o extends Error{constructor(t,e){const n=new.target.prototype;super(`${t}: Status code '${e}'`),this.statusCode=e,this.__proto__=n}}class i extends Error{constructor(t="A timeout occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class r extends Error{constructor(t="An abort occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class s extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.transport=e,this.errorType="UnsupportedTransportError",this.__proto__=n}}class a extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.transport=e,this.errorType="DisabledTransportError",this.__proto__=n}}class c extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.transport=e,this.errorType="FailedToStartTransportError",this.__proto__=n}}class l extends Error{constructor(t){const e=new.target.prototype;super(t),this.errorType="FailedToNegotiateWithServerError",this.__proto__=e}}class u extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.innerErrors=e,this.__proto__=n}}class d{constructor(t,e,n){this.statusCode=t,this.statusText=e,this.content=n}}class h{get(t,e){return this.send({...e,method:"GET",url:t})}post(t,e){return this.send({...e,method:"POST",url:t})}delete(t,e){return this.send({...e,method:"DELETE",url:t})}getCookieString(t){return""}}!function(t){t[t.Trace=0]="Trace",t[t.Debug=1]="Debug",t[t.Information=2]="Information",t[t.Warning=3]="Warning",t[t.Error=4]="Error",t[t.Critical=5]="Critical",t[t.None=6]="None"}(e||(e={}));class p{constructor(){}log(t,e){}}p.instance=new p;const f="10.0.0";class m{static isRequired(t,e){if(null==t)throw new Error(`The '${e}' argument is required.`)}static isNotEmpty(t,e){if(!t||t.match(/^\s*$/))throw new Error(`The '${e}' argument should not be empty.`)}static isIn(t,e,n){if(!(t in e))throw new Error(`Unknown ${n} value: ${t}.`)}}class g{static get isBrowser(){return!g.isNode&&"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return!g.isNode&&"object"==typeof self&&"importScripts"in self}static get isReactNative(){return!g.isNode&&"object"==typeof window&&void 0===window.document}static get isNode(){return"undefined"!=typeof process&&process.release&&"node"===process.release.name}}function y(t,e){let n="";return b(t)?(n=`Binary data of length ${t.byteLength}`,e&&(n+=`. Content: '${function(t){const e=new Uint8Array(t);let n="";return e.forEach((t=>{n+=`0x${t<16?"0":""}${t.toString(16)} `})),n.substring(0,n.length-1)}(t)}'`)):"string"==typeof t&&(n=`String data of length ${t.length}`,e&&(n+=`. Content: '${t}'`)),n}function b(t){return t&&"undefined"!=typeof ArrayBuffer&&(t instanceof ArrayBuffer||t.constructor&&"ArrayBuffer"===t.constructor.name)}async function v(t,n,o,i,r,s){const a={},[c,l]=C();a[c]=l,t.log(e.Trace,`(${n} transport) sending data. ${y(r,s.logMessageContent)}.`);const u=b(r)?"arraybuffer":"text",d=await o.post(i,{content:r,headers:{...a,...s.headers},responseType:u,timeout:s.timeout,withCredentials:s.withCredentials});t.log(e.Trace,`(${n} transport) request complete. Response status: ${d.statusCode}.`)}class ${constructor(t,e){this.i=t,this.h=e}dispose(){const t=this.i.observers.indexOf(this.h);t>-1&&this.i.observers.splice(t,1),0===this.i.observers.length&&this.i.cancelCallback&&this.i.cancelCallback().catch((t=>{}))}}class w{constructor(t){this.l=t,this.out=console}log(t,n){if(t>=this.l){const o=`[${(new Date).toISOString()}] ${e[t]}: ${n}`;switch(t){case e.Critical:case e.Error:this.out.error(o);break;case e.Warning:this.out.warn(o);break;case e.Information:this.out.info(o);break;default:this.out.log(o)}}}}function C(){let t="X-SignalR-User-Agent";return g.isNode&&(t="User-Agent"),[t,S(f,x(),g.isNode?"NodeJS":"Browser",T())]}function S(t,e,n,o){let i="Microsoft SignalR/";const r=t.split(".");return i+=`${r[0]}.${r[1]}`,i+=` (${t}; `,i+=e&&""!==e?`${e}; `:"Unknown OS; ",i+=`${n}`,i+=o?`; ${o}`:"; Unknown Runtime Version",i+=")",i}function x(){if(!g.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function T(){if(g.isNode)return process.versions.node}function _(t){return t.stack?t.stack:t.message?t.message:`${t}`}class E extends h{constructor(e){if(super(),this.u=e,"undefined"==typeof fetch||g.isNode){const t=require;this.p=new(t("tough-cookie").CookieJar),"undefined"==typeof fetch?this.m=t("node-fetch"):this.m=fetch,this.m=t("fetch-cookie")(this.m,this.p)}else this.m=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==t.g)return t.g;throw new Error("could not find global")}());if("undefined"==typeof AbortController){const t=require;this.v=t("abort-controller")}else this.v=AbortController}async send(t){if(t.abortSignal&&t.abortSignal.aborted)throw new r;if(!t.method)throw new Error("No method defined.");if(!t.url)throw new Error("No url defined.");const n=new this.v;let s;t.abortSignal&&(t.abortSignal.onabort=()=>{n.abort(),s=new r});let a,c=null;if(t.timeout){const o=t.timeout;c=setTimeout((()=>{n.abort(),this.u.log(e.Warning,"Timeout from HTTP request."),s=new i}),o)}""===t.content&&(t.content=void 0),t.content&&(t.headers=t.headers||{},b(t.content)?t.headers["Content-Type"]="application/octet-stream":t.headers["Content-Type"]="text/plain;charset=UTF-8");try{a=await this.m(t.url,{body:t.content,cache:"no-cache",credentials:!0===t.withCredentials?"include":"same-origin",headers:{"X-Requested-With":"XMLHttpRequest",...t.headers},method:t.method,mode:"cors",redirect:"follow",signal:n.signal})}catch(t){if(s)throw s;throw this.u.log(e.Warning,`Error from HTTP request. ${t}.`),t}finally{c&&clearTimeout(c),t.abortSignal&&(t.abortSignal.onabort=null)}if(!a.ok){const t=await I(a,"text");throw new o(t||a.statusText,a.status)}const l=I(a,t.responseType),u=await l;return new d(a.status,a.statusText,u)}getCookieString(t){let e="";return g.isNode&&this.p&&this.p.getCookies(t,((t,n)=>e=n.join("; "))),e}}function I(t,e){let n;switch(e){case"arraybuffer":n=t.arrayBuffer();break;case"text":default:n=t.text();break;case"blob":case"document":case"json":throw new Error(`${e} is not supported.`)}return n}class k extends h{constructor(t){super(),this.u=t}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?new Promise(((n,s)=>{const a=new XMLHttpRequest;a.open(t.method,t.url,!0),a.withCredentials=void 0===t.withCredentials||t.withCredentials,a.setRequestHeader("X-Requested-With","XMLHttpRequest"),""===t.content&&(t.content=void 0),t.content&&(b(t.content)?a.setRequestHeader("Content-Type","application/octet-stream"):a.setRequestHeader("Content-Type","text/plain;charset=UTF-8"));const c=t.headers;c&&Object.keys(c).forEach((t=>{a.setRequestHeader(t,c[t])})),t.responseType&&(a.responseType=t.responseType),t.abortSignal&&(t.abortSignal.onabort=()=>{a.abort(),s(new r)}),t.timeout&&(a.timeout=t.timeout),a.onload=()=>{t.abortSignal&&(t.abortSignal.onabort=null),a.status>=200&&a.status<300?n(new d(a.status,a.statusText,a.response||a.responseText)):s(new o(a.response||a.responseText||a.statusText,a.status))},a.onerror=()=>{this.u.log(e.Warning,`Error from HTTP request. ${a.status}: ${a.statusText}.`),s(new o(a.statusText,a.status))},a.ontimeout=()=>{this.u.log(e.Warning,"Timeout from HTTP request."),s(new i)},a.send(t.content)})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class D extends h{constructor(t){if(super(),"undefined"!=typeof fetch||g.isNode)this.$=new E(t);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this.$=new k(t)}}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?this.$.send(t):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(t){return this.$.getCookieString(t)}}class A{static write(t){return`${t}${A.RecordSeparator}`}static parse(t){if(t[t.length-1]!==A.RecordSeparator)throw new Error("Message is incomplete.");const e=t.split(A.RecordSeparator);return e.pop(),e}}A.RecordSeparatorCode=30,A.RecordSeparator=String.fromCharCode(A.RecordSeparatorCode);class P{writeHandshakeRequest(t){return A.write(JSON.stringify(t))}parseHandshakeResponse(t){let e,n;if(b(t)){const o=new Uint8Array(t),i=o.indexOf(A.RecordSeparatorCode);if(-1===i)throw new Error("Message is incomplete.");const r=i+1;e=String.fromCharCode.apply(null,Array.prototype.slice.call(o.slice(0,r))),n=o.byteLength>r?o.slice(r).buffer:null}else{const o=t,i=o.indexOf(A.RecordSeparator);if(-1===i)throw new Error("Message is incomplete.");const r=i+1;e=o.substring(0,r),n=o.length>r?o.substring(r):null}const o=A.parse(e),i=JSON.parse(o[0]);if(i.type)throw new Error("Expected a handshake response from the server.");return[n,i]}}var N,j;!function(t){t[t.Invocation=1]="Invocation",t[t.StreamItem=2]="StreamItem",t[t.Completion=3]="Completion",t[t.StreamInvocation=4]="StreamInvocation",t[t.CancelInvocation=5]="CancelInvocation",t[t.Ping=6]="Ping",t[t.Close=7]="Close",t[t.Ack=8]="Ack",t[t.Sequence=9]="Sequence"}(N||(N={}));class q{constructor(){this.observers=[]}next(t){for(const e of this.observers)e.next(t)}error(t){for(const e of this.observers)e.error&&e.error(t)}complete(){for(const t of this.observers)t.complete&&t.complete()}subscribe(t){return this.observers.push(t),new $(this,t)}}class M{constructor(t,e,n){this.C=1e5,this.S=[],this.k=0,this.P=!1,this.T=1,this.I=0,this._=0,this.H=!1,this.D=t,this.R=e,this.C=n}async A(t){const e=this.D.writeMessage(t);let n=Promise.resolve();if(this.U(t)){this.k++;let t=()=>{},o=()=>{};b(e)?this._+=e.byteLength:this._+=e.length,this._>=this.C&&(n=new Promise(((e,n)=>{t=e,o=n}))),this.S.push(new R(e,this.k,t,o))}try{this.H||await this.R.send(e)}catch{this.L()}await n}N(t){let e=-1;for(let n=0;nthis.T?this.R.stop(new Error("Sequence ID greater than amount of messages we've received.")):this.T=t.sequenceId}L(){this.H=!0,this.P=!0}async B(){const t=0!==this.S.length?this.S[0].q:this.k+1;await this.R.send(this.D.writeMessage({type:N.Sequence,sequenceId:t}));const e=this.S;for(const t of e)await this.R.send(t.M);this.H=!1}X(t){null!=t||(t=new Error("Unable to reconnect to server."));for(const e of this.S)e.J(t)}U(t){switch(t.type){case N.Invocation:case N.StreamItem:case N.Completion:case N.StreamInvocation:case N.CancelInvocation:return!0;case N.Close:case N.Sequence:case N.Ping:case N.Ack:return!1}}O(){void 0===this.V&&(this.V=setTimeout((async()=>{try{this.H||await this.R.send(this.D.writeMessage({type:N.Ack,sequenceId:this.I}))}catch{}clearTimeout(this.V),this.V=void 0}),1e3))}}class R{constructor(t,e,n,o){this.M=t,this.q=e,this.j=n,this.J=o}}!function(t){t.Disconnected="Disconnected",t.Connecting="Connecting",t.Connected="Connected",t.Disconnecting="Disconnecting",t.Reconnecting="Reconnecting"}(j||(j={}));class O{static create(t,e,n,o,i,r,s){return new O(t,e,n,o,i,r,s)}constructor(t,n,o,i,r,s,a){this.K=0,this.G=()=>{this.u.log(e.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://learn.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},m.isRequired(t,"connection"),m.isRequired(n,"logger"),m.isRequired(o,"protocol"),this.serverTimeoutInMilliseconds=null!=r?r:3e4,this.keepAliveIntervalInMilliseconds=null!=s?s:15e3,this.Y=null!=a?a:1e5,this.u=n,this.D=o,this.connection=t,this.Z=i,this.tt=new P,this.connection.onreceive=t=>this.et(t),this.connection.onclose=t=>this.st(t),this.it={},this.nt={},this.rt=[],this.ot=[],this.ht=[],this.ct=0,this.lt=!1,this.ut=j.Disconnected,this.dt=!1,this.ft=this.D.writeMessage({type:N.Ping})}get state(){return this.ut}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(t){if(this.ut!==j.Disconnected&&this.ut!==j.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!t)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=t}start(){return this.wt=this.gt(),this.wt}async gt(){if(this.ut!==j.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this.ut=j.Connecting,this.u.log(e.Debug,"Starting HubConnection.");try{await this.yt(),g.isBrowser&&window.document.addEventListener("freeze",this.G),this.ut=j.Connected,this.dt=!0,this.u.log(e.Debug,"HubConnection connected successfully.")}catch(t){return this.ut=j.Disconnected,this.u.log(e.Debug,`HubConnection failed to start successfully because of error '${t}'.`),Promise.reject(t)}}async yt(){this.vt=void 0,this.lt=!1;const t=new Promise(((t,e)=>{this.bt=t,this.Et=e}));await this.connection.start(this.D.transferFormat);try{let n=this.D.version;this.connection.features.reconnect||(n=1);const o={protocol:this.D.name,version:n};if(this.u.log(e.Debug,"Sending handshake request."),await this.$t(this.tt.writeHandshakeRequest(o)),this.u.log(e.Information,`Using HubProtocol '${this.D.name}'.`),this.Ct(),this.St(),this.kt(),await t,this.vt)throw this.vt;!!this.connection.features.reconnect&&(this.Pt=new M(this.D,this.connection,this.Y),this.connection.features.disconnected=this.Pt.L.bind(this.Pt),this.connection.features.resend=()=>{if(this.Pt)return this.Pt.B()}),this.connection.features.inherentKeepAlive||await this.$t(this.ft)}catch(t){throw this.u.log(e.Debug,`Hub handshake failed with error '${t}' during start(). Stopping HubConnection.`),this.Ct(),this.Tt(),await this.connection.stop(t),t}}async stop(){const t=this.wt;this.connection.features.reconnect=!1,this.It=this._t(),await this.It;try{await t}catch(t){}}_t(t){if(this.ut===j.Disconnected)return this.u.log(e.Debug,`Call to HubConnection.stop(${t}) ignored because it is already in the disconnected state.`),Promise.resolve();if(this.ut===j.Disconnecting)return this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.It;const n=this.ut;return this.ut=j.Disconnecting,this.u.log(e.Debug,"Stopping HubConnection."),this.Ht?(this.u.log(e.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this.Ht),this.Ht=void 0,this.Dt(),Promise.resolve()):(n===j.Connected&&this.Rt(),this.Ct(),this.Tt(),this.vt=t||new r("The connection was stopped before the hub handshake could complete."),this.connection.stop(t))}async Rt(){try{await this.xt(this.At())}catch{}}stream(t,...e){const[n,o]=this.Ut(e),i=this.Lt(t,e,o);let r;const s=new q;return s.cancelCallback=()=>{const t=this.Nt(i.invocationId);return delete this.it[i.invocationId],r.then((()=>this.xt(t)))},this.it[i.invocationId]=(t,e)=>{e?s.error(e):t&&(t.type===N.Completion?t.error?s.error(new Error(t.error)):s.complete():s.next(t.item))},r=this.xt(i).catch((t=>{s.error(t),delete this.it[i.invocationId]})),this.qt(n,r),s}$t(t){return this.kt(),this.connection.send(t)}xt(t){return this.Pt?this.Pt.A(t):this.$t(this.D.writeMessage(t))}send(t,...e){const[n,o]=this.Ut(e),i=this.xt(this.Mt(t,e,!0,o));return this.qt(n,i),i}invoke(t,...e){const[n,o]=this.Ut(e),i=this.Mt(t,e,!1,o);return new Promise(((t,e)=>{this.it[i.invocationId]=(n,o)=>{o?e(o):n&&(n.type===N.Completion?n.error?e(new Error(n.error)):t(n.result):e(new Error(`Unexpected message type: ${n.type}`)))};const o=this.xt(i).catch((t=>{e(t),delete this.it[i.invocationId]}));this.qt(n,o)}))}on(t,e){t&&e&&(t=t.toLowerCase(),this.nt[t]||(this.nt[t]=[]),-1===this.nt[t].indexOf(e)&&this.nt[t].push(e))}off(t,e){if(!t)return;t=t.toLowerCase();const n=this.nt[t];if(n)if(e){const o=n.indexOf(e);-1!==o&&(n.splice(o,1),0===n.length&&delete this.nt[t])}else delete this.nt[t]}onclose(t){t&&this.rt.push(t)}onreconnecting(t){t&&this.ot.push(t)}onreconnected(t){t&&this.ht.push(t)}et(t){if(this.Ct(),this.lt||(t=this.jt(t),this.lt=!0),t){const n=this.D.parseMessages(t,this.u);for(const o of n)if(!this.Pt||this.Pt.W(o))switch(o.type){case N.Invocation:this.Wt(o).catch((t=>{this.u.log(e.Error,`Invoke client method threw error: ${_(t)}`)}));break;case N.StreamItem:case N.Completion:{const n=this.it[o.invocationId];if(n){o.type===N.Completion&&delete this.it[o.invocationId];try{n(o)}catch(t){this.u.log(e.Error,`Stream callback threw error: ${_(t)}`)}}break}case N.Ping:break;case N.Close:{this.u.log(e.Information,"Close message received from server.");const t=o.error?new Error("Server returned an error on close: "+o.error):void 0;!0===o.allowReconnect?this.connection.stop(t):this.It=this._t(t);break}case N.Ack:this.Pt&&this.Pt.N(o);break;case N.Sequence:this.Pt&&this.Pt.F(o);break;default:this.u.log(e.Warning,`Invalid message type: ${o.type}.`)}}this.St()}jt(t){let n,o;try{[o,n]=this.tt.parseHandshakeResponse(t)}catch(t){const n="Error parsing handshake response: "+t;this.u.log(e.Error,n);const o=new Error(n);throw this.Et(o),o}if(n.error){const t="Server returned handshake error: "+n.error;this.u.log(e.Error,t);const o=new Error(t);throw this.Et(o),o}return this.u.log(e.Debug,"Server handshake complete."),this.bt(),o}kt(){this.connection.features.inherentKeepAlive||(this.K=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this.Tt())}St(){if(!this.connection.features||!this.connection.features.inherentKeepAlive){this.Ot=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds);let t=this.K-(new Date).getTime();if(t<0)return void(this.ut===j.Connected&&this.Ft());void 0===this.Bt&&(t<0&&(t=0),this.Bt=setTimeout((async()=>{this.ut===j.Connected&&await this.Ft()}),t))}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}async Wt(t){const n=t.target.toLowerCase(),o=this.nt[n];if(!o)return this.u.log(e.Warning,`No client method with the name '${n}' found.`),void(t.invocationId&&(this.u.log(e.Warning,`No result given for '${n}' method and invocation ID '${t.invocationId}'.`),await this.xt(this.Xt(t.invocationId,"Client didn't provide a result.",null))));const i=o.slice(),r=!!t.invocationId;let s,a,c;for(const o of i)try{const i=s;s=await o.apply(this,t.arguments),r&&s&&i&&(this.u.log(e.Error,`Multiple results provided for '${n}'. Sending error to server.`),c=this.Xt(t.invocationId,"Client provided multiple results.",null)),a=void 0}catch(t){a=t,this.u.log(e.Error,`A callback for the method '${n}' threw error '${t}'.`)}c?await this.xt(c):r?(a?c=this.Xt(t.invocationId,`${a}`,null):void 0!==s?c=this.Xt(t.invocationId,null,s):(this.u.log(e.Warning,`No result given for '${n}' method and invocation ID '${t.invocationId}'.`),c=this.Xt(t.invocationId,"Client didn't provide a result.",null)),await this.xt(c)):s&&this.u.log(e.Error,`Result given for '${n}' method but server is not expecting a result.`)}st(t){this.u.log(e.Debug,`HubConnection.connectionClosed(${t}) called while in state ${this.ut}.`),this.vt=this.vt||t||new r("The underlying connection was closed before the hub handshake could complete."),this.bt&&this.bt(),this.Jt(t||new Error("Invocation canceled due to the underlying connection being closed.")),this.Ct(),this.Tt(),this.ut===j.Disconnecting?this.Dt(t):this.ut===j.Connected&&this.Z?this.zt(t):this.ut===j.Connected&&this.Dt(t)}Dt(t){if(this.dt){this.ut=j.Disconnected,this.dt=!1,this.Pt&&(this.Pt.X(null!=t?t:new Error("Connection closed.")),this.Pt=void 0),g.isBrowser&&window.document.removeEventListener("freeze",this.G);try{this.rt.forEach((e=>e.apply(this,[t])))}catch(n){this.u.log(e.Error,`An onclose callback called with error '${t}' threw error '${n}'.`)}}}async zt(t){const n=Date.now();let o=0,i=void 0!==t?t:new Error("Attempting to reconnect due to a unknown error."),r=this.Vt(o,0,i);if(null===r)return this.u.log(e.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this.Dt(t);if(this.ut=j.Reconnecting,t?this.u.log(e.Information,`Connection reconnecting because of error '${t}'.`):this.u.log(e.Information,"Connection reconnecting."),0!==this.ot.length){try{this.ot.forEach((e=>e.apply(this,[t])))}catch(n){this.u.log(e.Error,`An onreconnecting callback called with error '${t}' threw error '${n}'.`)}if(this.ut!==j.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==r;){if(this.u.log(e.Information,`Reconnect attempt number ${o+1} will start in ${r} ms.`),await new Promise((t=>{this.Ht=setTimeout(t,r)})),this.Ht=void 0,this.ut!==j.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this.yt(),this.ut=j.Connected,this.u.log(e.Information,"HubConnection reconnected successfully."),0!==this.ht.length)try{this.ht.forEach((t=>t.apply(this,[this.connection.connectionId])))}catch(t){this.u.log(e.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${t}'.`)}return}catch(t){if(this.u.log(e.Information,`Reconnect attempt failed because of error '${t}'.`),this.ut!==j.Reconnecting)return this.u.log(e.Debug,`Connection moved to the '${this.ut}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this.ut===j.Disconnecting&&this.Dt());o++,i=t instanceof Error?t:new Error(t.toString()),r=this.Vt(o,Date.now()-n,i)}}this.u.log(e.Information,`Reconnect retries have been exhausted after ${Date.now()-n} ms and ${o} failed attempts. Connection disconnecting.`),this.Dt()}Vt(t,n,o){try{return this.Z.nextRetryDelayInMilliseconds({elapsedMilliseconds:n,previousRetryCount:t,retryReason:o})}catch(o){return this.u.log(e.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${t}, ${n}) threw error '${o}'.`),null}}Jt(t){const n=this.it;this.it={},Object.keys(n).forEach((o=>{const i=n[o];try{i(null,t)}catch(n){this.u.log(e.Error,`Stream 'error' callback called with '${t}' threw error: ${_(n)}`)}}))}Tt(){this.Bt&&(clearTimeout(this.Bt),this.Bt=void 0)}Ct(){this.Ot&&clearTimeout(this.Ot)}Mt(t,e,n,o){if(n)return 0!==o.length?{target:t,arguments:e,streamIds:o,type:N.Invocation}:{target:t,arguments:e,type:N.Invocation};{const n=this.ct;return this.ct++,0!==o.length?{target:t,arguments:e,invocationId:n.toString(),streamIds:o,type:N.Invocation}:{target:t,arguments:e,invocationId:n.toString(),type:N.Invocation}}}qt(t,e){if(0!==t.length){e||(e=Promise.resolve());for(const n in t)t[n].subscribe({complete:()=>{e=e.then((()=>this.xt(this.Xt(n))))},error:t=>{let o;o=t instanceof Error?t.message:t&&t.toString?t.toString():"Unknown error",e=e.then((()=>this.xt(this.Xt(n,o))))},next:t=>{e=e.then((()=>this.xt(this.Kt(n,t))))}})}}Ut(t){const e=[],n=[];for(let o=0;o0)&&(e=!1,this.te=await this.Zt()),this.ee(t);const n=await this.Yt.send(t);return e&&401===n.statusCode&&this.Zt?(this.te=await this.Zt(),this.ee(t),await this.Yt.send(t)):n}ee(t){t.headers||(t.headers={}),this.te?t.headers[L.Authorization]=`Bearer ${this.te}`:this.Zt&&t.headers[L.Authorization]&&delete t.headers[L.Authorization]}getCookieString(t){return this.Yt.getCookieString(t)}}var U,B;!function(t){t[t.None=0]="None",t[t.WebSockets=1]="WebSockets",t[t.ServerSentEvents=2]="ServerSentEvents",t[t.LongPolling=4]="LongPolling"}(U||(U={})),function(t){t[t.Text=1]="Text",t[t.Binary=2]="Binary"}(B||(B={}));class W{constructor(){this.se=!1,this.onabort=null}abort(){this.se||(this.se=!0,this.onabort&&this.onabort())}get signal(){return this}get aborted(){return this.se}}class X{get pollAborted(){return this.ie.aborted}constructor(t,e,n){this.$=t,this.u=e,this.ie=new W,this.ne=n,this.re=!1,this.onreceive=null,this.onclose=null}async connect(t,n){if(m.isRequired(t,"url"),m.isRequired(n,"transferFormat"),m.isIn(n,B,"transferFormat"),this.oe=t,this.u.log(e.Trace,"(LongPolling transport) Connecting."),n===B.Binary&&"undefined"!=typeof XMLHttpRequest&&"string"!=typeof(new XMLHttpRequest).responseType)throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");const[i,r]=C(),s={[i]:r,...this.ne.headers},a={abortSignal:this.ie.signal,headers:s,timeout:1e5,withCredentials:this.ne.withCredentials};n===B.Binary&&(a.responseType="arraybuffer");const c=`${t}&_=${Date.now()}`;this.u.log(e.Trace,`(LongPolling transport) polling: ${c}.`);const l=await this.$.get(c,a);200!==l.statusCode?(this.u.log(e.Error,`(LongPolling transport) Unexpected response code: ${l.statusCode}.`),this.he=new o(l.statusText||"",l.statusCode),this.re=!1):this.re=!0,this.ce=this.ae(this.oe,a)}async ae(t,n){try{for(;this.re;)try{const i=`${t}&_=${Date.now()}`;this.u.log(e.Trace,`(LongPolling transport) polling: ${i}.`);const r=await this.$.get(i,n);204===r.statusCode?(this.u.log(e.Information,"(LongPolling transport) Poll terminated by server."),this.re=!1):200!==r.statusCode?(this.u.log(e.Error,`(LongPolling transport) Unexpected response code: ${r.statusCode}.`),this.he=new o(r.statusText||"",r.statusCode),this.re=!1):r.content?(this.u.log(e.Trace,`(LongPolling transport) data received. ${y(r.content,this.ne.logMessageContent)}.`),this.onreceive&&this.onreceive(r.content)):this.u.log(e.Trace,"(LongPolling transport) Poll timed out, reissuing.")}catch(t){this.re?t instanceof i?this.u.log(e.Trace,"(LongPolling transport) Poll timed out, reissuing."):(this.he=t,this.re=!1):this.u.log(e.Trace,`(LongPolling transport) Poll errored after shutdown: ${t.message}`)}}finally{this.u.log(e.Trace,"(LongPolling transport) Polling complete."),this.pollAborted||this.le()}}async send(t){return this.re?v(this.u,"LongPolling",this.$,this.oe,t,this.ne):Promise.reject(new Error("Cannot send until the transport is connected"))}async stop(){this.u.log(e.Trace,"(LongPolling transport) Stopping polling."),this.re=!1,this.ie.abort();try{await this.ce,this.u.log(e.Trace,`(LongPolling transport) sending DELETE request to ${this.oe}.`);const t={},[n,i]=C();t[n]=i;const r={headers:{...t,...this.ne.headers},timeout:this.ne.timeout,withCredentials:this.ne.withCredentials};let s;try{await this.$.delete(this.oe,r)}catch(t){s=t}s?s instanceof o&&(404===s.statusCode?this.u.log(e.Trace,"(LongPolling transport) A 404 response was returned from sending a DELETE request."):this.u.log(e.Trace,`(LongPolling transport) Error sending a DELETE request: ${s}`)):this.u.log(e.Trace,"(LongPolling transport) DELETE request accepted.")}finally{this.u.log(e.Trace,"(LongPolling transport) Stop finished."),this.le()}}le(){if(this.onclose){let t="(LongPolling transport) Firing onclose event.";this.he&&(t+=" Error: "+this.he),this.u.log(e.Trace,t),this.onclose(this.he)}}}class V{constructor(t,e,n,o){this.$=t,this.te=e,this.u=n,this.ne=o,this.onreceive=null,this.onclose=null}async connect(t,n){return m.isRequired(t,"url"),m.isRequired(n,"transferFormat"),m.isIn(n,B,"transferFormat"),this.u.log(e.Trace,"(SSE transport) Connecting."),this.oe=t,this.te&&(t+=(t.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(this.te)}`),new Promise(((o,i)=>{let r,s=!1;if(n===B.Text){if(g.isBrowser||g.isWebWorker)r=new this.ne.EventSource(t,{withCredentials:this.ne.withCredentials});else{const e=this.$.getCookieString(t),n={};n.Cookie=e;const[o,i]=C();n[o]=i,r=new this.ne.EventSource(t,{withCredentials:this.ne.withCredentials,headers:{...n,...this.ne.headers}})}try{r.onmessage=t=>{if(this.onreceive)try{this.u.log(e.Trace,`(SSE transport) data received. ${y(t.data,this.ne.logMessageContent)}.`),this.onreceive(t.data)}catch(t){return void this.ue(t)}},r.onerror=t=>{s?this.ue():i(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},r.onopen=()=>{this.u.log(e.Information,`SSE connected to ${this.oe}`),this.de=r,s=!0,o()}}catch(t){return void i(t)}}else i(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(t){return this.de?v(this.u,"SSE",this.$,this.oe,t,this.ne):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this.ue(),Promise.resolve()}ue(t){this.de&&(this.de.close(),this.de=void 0,this.onclose&&this.onclose(t))}}class Z{constructor(t,e,n,o,i,r){this.u=n,this.Zt=e,this.fe=o,this.pe=i,this.$=t,this.onreceive=null,this.onclose=null,this.we=r}async connect(t,n){let o;return m.isRequired(t,"url"),m.isRequired(n,"transferFormat"),m.isIn(n,B,"transferFormat"),this.u.log(e.Trace,"(WebSockets transport) Connecting."),this.Zt&&(o=await this.Zt()),new Promise(((i,r)=>{let s;t=t.replace(/^http/,"ws");const a=this.$.getCookieString(t);let c=!1;if(g.isNode||g.isReactNative){const e={},[n,i]=C();e[n]=i,o&&(e[L.Authorization]=`Bearer ${o}`),a&&(e[L.Cookie]=a),s=new this.pe(t,void 0,{headers:{...e,...this.we}})}else o&&(t+=(t.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(o)}`);s||(s=new this.pe(t)),n===B.Binary&&(s.binaryType="arraybuffer"),s.onopen=n=>{this.u.log(e.Information,`WebSocket connected to ${t}.`),this.ge=s,c=!0,i()},s.onerror=t=>{let n=null;n="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"There was an error with the transport",this.u.log(e.Information,`(WebSockets transport) ${n}.`)},s.onmessage=t=>{if(this.u.log(e.Trace,`(WebSockets transport) data received. ${y(t.data,this.fe)}.`),this.onreceive)try{this.onreceive(t.data)}catch(t){return void this.ue(t)}},s.onclose=t=>{if(c)this.ue(t);else{let e=null;e="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",r(new Error(e))}}}))}send(t){return this.ge&&this.ge.readyState===this.pe.OPEN?(this.u.log(e.Trace,`(WebSockets transport) sending data. ${y(t,this.fe)}.`),this.ge.send(t),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this.ge&&this.ue(void 0),Promise.resolve()}ue(t){this.ge&&(this.ge.onclose=()=>{},this.ge.onmessage=()=>{},this.ge.onerror=()=>{},this.ge.close(),this.ge=void 0),this.u.log(e.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this.me(t)||!1!==t.wasClean&&1e3===t.code?t instanceof Error?this.onclose(t):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${t.code} (${t.reason||"no reason given"}).`)))}me(t){return t&&"boolean"==typeof t.wasClean&&"number"==typeof t.code}}class J{constructor(t,n={}){var o;if(this.ye=()=>{},this.features={},this.ve=1,m.isRequired(t,"url"),this.u=void 0===(o=n.logger)?new w(e.Information):null===o?p.instance:void 0!==o.log?o:new w(o),this.baseUrl=this.be(t),(n=n||{}).logMessageContent=void 0!==n.logMessageContent&&n.logMessageContent,"boolean"!=typeof n.withCredentials&&void 0!==n.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");n.withCredentials=void 0===n.withCredentials||n.withCredentials,n.timeout=void 0===n.timeout?1e5:n.timeout;let i=null,r=null;if(g.isNode){const t=require;i=t("ws"),r=t("eventsource")}g.isNode||"undefined"==typeof WebSocket||n.WebSocket?g.isNode&&!n.WebSocket&&i&&(n.WebSocket=i):n.WebSocket=WebSocket,g.isNode||"undefined"==typeof EventSource||n.EventSource?g.isNode&&!n.EventSource&&void 0!==r&&(n.EventSource=r):n.EventSource=EventSource,this.$=new F(n.httpClient||new D(this.u),n.accessTokenFactory),this.ut="Disconnected",this.dt=!1,this.ne=n,this.onreceive=null,this.onclose=null}async start(t){if(t=t||B.Binary,m.isIn(t,B,"transferFormat"),this.u.log(e.Debug,`Starting connection with transfer format '${B[t]}'.`),"Disconnected"!==this.ut)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this.ut="Connecting",this.Ee=this.yt(t),await this.Ee,"Disconnecting"===this.ut){const t="Failed to start the HttpConnection before stop() was called.";return this.u.log(e.Error,t),await this.It,Promise.reject(new r(t))}if("Connected"!==this.ut){const t="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this.u.log(e.Error,t),Promise.reject(new r(t))}this.dt=!0}send(t){return"Connected"!==this.ut?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this.$e||(this.$e=new Y(this.transport)),this.$e.send(t))}async stop(t){return"Disconnected"===this.ut?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this.ut?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.It):(this.ut="Disconnecting",this.It=new Promise((t=>{this.ye=t})),await this._t(t),void await this.It)}async _t(t){this.Ce=t;try{await this.Ee}catch(t){}if(this.transport){try{await this.transport.stop()}catch(t){this.u.log(e.Error,`HttpConnection.transport.stop() threw error '${t}'.`),this.Se()}this.transport=void 0}else this.u.log(e.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async yt(t){let n=this.baseUrl;this.Zt=this.ne.accessTokenFactory,this.$.Zt=this.Zt;try{if(this.ne.skipNegotiation){if(this.ne.transport!==U.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this.ke(U.WebSockets),await this.Pe(n,t)}else{let e=null,o=0;do{if(e=await this.Te(n),"Disconnecting"===this.ut||"Disconnected"===this.ut)throw new r("The connection was stopped during negotiation.");if(e.error)throw new Error(e.error);if(e.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(e.url&&(n=e.url),e.accessToken){const t=e.accessToken;this.Zt=()=>t,this.$.te=t,this.$.Zt=void 0}o++}while(e.url&&o<100);if(100===o&&e.url)throw new Error("Negotiate redirection limit exceeded.");await this.Ie(n,this.ne.transport,e,t)}this.transport instanceof X&&(this.features.inherentKeepAlive=!0),"Connecting"===this.ut&&(this.u.log(e.Debug,"The HttpConnection connected successfully."),this.ut="Connected")}catch(t){return this.u.log(e.Error,"Failed to start the connection: "+t),this.ut="Disconnected",this.transport=void 0,this.ye(),Promise.reject(t)}}async Te(t){const n={},[i,r]=C();n[i]=r;const s=this._e(t);this.u.log(e.Debug,`Sending negotiation request: ${s}.`);try{const t=await this.$.post(s,{content:"",headers:{...n,...this.ne.headers},timeout:this.ne.timeout,withCredentials:this.ne.withCredentials});if(200!==t.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${t.statusCode}'`));const e=JSON.parse(t.content);return(!e.negotiateVersion||e.negotiateVersion<1)&&(e.connectionToken=e.connectionId),e.useStatefulReconnect&&!0!==this.ne.He?Promise.reject(new l("Client didn't negotiate Stateful Reconnect but the server did.")):e}catch(t){let n="Failed to complete negotiation with the server: "+t;return t instanceof o&&404===t.statusCode&&(n+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this.u.log(e.Error,n),Promise.reject(new l(n))}}De(t,e){return e?t+(-1===t.indexOf("?")?"?":"&")+`id=${e}`:t}async Ie(t,n,o,i){let s=this.De(t,o.connectionToken);if(this.Re(n))return this.u.log(e.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=n,await this.Pe(s,i),void(this.connectionId=o.connectionId);const a=[],l=o.availableTransports||[];let d=o;for(const o of l){const l=this.xe(o,n,i,!0===(null==d?void 0:d.useStatefulReconnect));if(l instanceof Error)a.push(`${o.transport} failed:`),a.push(l);else if(this.Re(l)){if(this.transport=l,!d){try{d=await this.Te(t)}catch(t){return Promise.reject(t)}s=this.De(t,d.connectionToken)}try{return await this.Pe(s,i),void(this.connectionId=d.connectionId)}catch(t){if(this.u.log(e.Error,`Failed to start the transport '${o.transport}': ${t}`),d=void 0,a.push(new c(`${o.transport} failed: ${t}`,U[o.transport])),"Connecting"!==this.ut){const t="Failed to select transport before stop() was called.";return this.u.log(e.Debug,t),Promise.reject(new r(t))}}}}return a.length>0?Promise.reject(new u(`Unable to connect to the server with any of the available transports. ${a.join(" ")}`,a)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}ke(t){switch(t){case U.WebSockets:if(!this.ne.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new Z(this.$,this.Zt,this.u,this.ne.logMessageContent,this.ne.WebSocket,this.ne.headers||{});case U.ServerSentEvents:if(!this.ne.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new V(this.$,this.$.te,this.u,this.ne);case U.LongPolling:return new X(this.$,this.u,this.ne);default:throw new Error(`Unknown transport: ${t}.`)}}Pe(t,e){return this.transport.onreceive=this.onreceive,this.features.reconnect?this.transport.onclose=async n=>{let o=!1;if(this.features.reconnect){try{this.features.disconnected(),await this.transport.connect(t,e),await this.features.resend()}catch{o=!0}o&&this.Se(n)}else this.Se(n)}:this.transport.onclose=t=>this.Se(t),this.transport.connect(t,e)}xe(t,n,o,i){const r=U[t.transport];if(null==r)return this.u.log(e.Debug,`Skipping transport '${t.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${t.transport}' because it is not supported by this client.`);if(!function(t,e){return!t||!!(e&t)}(n,r))return this.u.log(e.Debug,`Skipping transport '${U[r]}' because it was disabled by the client.`),new a(`'${U[r]}' is disabled by the client.`,r);if(!(t.transferFormats.map((t=>B[t])).indexOf(o)>=0))return this.u.log(e.Debug,`Skipping transport '${U[r]}' because it does not support the requested transfer format '${B[o]}'.`),new Error(`'${U[r]}' does not support ${B[o]}.`);if(r===U.WebSockets&&!this.ne.WebSocket||r===U.ServerSentEvents&&!this.ne.EventSource)return this.u.log(e.Debug,`Skipping transport '${U[r]}' because it is not supported in your environment.'`),new s(`'${U[r]}' is not supported in your environment.`,r);this.u.log(e.Debug,`Selecting transport '${U[r]}'.`);try{return this.features.reconnect=r===U.WebSockets?i:void 0,this.ke(r)}catch(t){return t}}Re(t){return t&&"object"==typeof t&&"connect"in t}Se(t){if(this.u.log(e.Debug,`HttpConnection.stopConnection(${t}) called while in state ${this.ut}.`),this.transport=void 0,t=this.Ce||t,this.Ce=void 0,"Disconnected"!==this.ut){if("Connecting"===this.ut)throw this.u.log(e.Warning,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${t}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this.ut&&this.ye(),t?this.u.log(e.Error,`Connection disconnected with error '${t}'.`):this.u.log(e.Information,"Connection disconnected."),this.$e&&(this.$e.stop().catch((t=>{this.u.log(e.Error,`TransportSendQueue.stop() threw error '${t}'.`)})),this.$e=void 0),this.connectionId=void 0,this.ut="Disconnected",this.dt){this.dt=!1;try{this.onclose&&this.onclose(t)}catch(n){this.u.log(e.Error,`HttpConnection.onclose(${t}) threw error '${n}'.`)}}}else this.u.log(e.Debug,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is already in the disconnected state.`)}be(t){if(0===t.lastIndexOf("https://",0)||0===t.lastIndexOf("http://",0))return t;if(!g.isBrowser)throw new Error(`Cannot resolve '${t}'.`);const n=window.document.createElement("a");return n.href=t,this.u.log(e.Information,`Normalizing '${t}' to '${n.href}'.`),n.href}_e(t){const e=new URL(t);e.pathname.endsWith("/")?e.pathname+="negotiate":e.pathname+="/negotiate";const n=new URLSearchParams(e.searchParams);return n.has("negotiateVersion")||n.append("negotiateVersion",this.ve.toString()),n.has("useStatefulReconnect")?"true"===n.get("useStatefulReconnect")&&(this.ne.He=!0):!0===this.ne.He&&n.append("useStatefulReconnect","true"),e.search=n.toString(),e.toString()}}class Y{constructor(t){this.Ae=t,this.Ue=[],this.Le=!0,this.Ne=new Q,this.qe=new Q,this.Me=this.je()}send(t){return this.We(t),this.qe||(this.qe=new Q),this.qe.promise}stop(){return this.Le=!1,this.Ne.resolve(),this.Me}We(t){if(this.Ue.length&&typeof this.Ue[0]!=typeof t)throw new Error(`Expected data to be of type ${typeof this.Ue} but was of type ${typeof t}`);this.Ue.push(t),this.Ne.resolve()}async je(){for(;;){if(await this.Ne.promise,!this.Le){this.qe&&this.qe.reject("Connection stopped.");break}this.Ne=new Q;const t=this.qe;this.qe=void 0;const e="string"==typeof this.Ue[0]?this.Ue.join(""):Y.Oe(this.Ue);this.Ue.length=0;try{await this.Ae.send(e),t.resolve()}catch(e){t.reject(e)}}}static Oe(t){const e=t.map((t=>t.byteLength)).reduce(((t,e)=>t+e)),n=new Uint8Array(e);let o=0;for(const e of t)n.set(new Uint8Array(e),o),o+=e.byteLength;return n.buffer}}class Q{constructor(){this.promise=new Promise(((t,e)=>[this.j,this.Fe]=[t,e]))}resolve(){this.j()}reject(t){this.Fe(t)}}class K{constructor(){this.name="json",this.version=2,this.transferFormat=B.Text}parseMessages(t,n){if("string"!=typeof t)throw new Error("Invalid input for JSON hub protocol. Expected a string.");if(!t)return[];null===n&&(n=p.instance);const o=A.parse(t),i=[];for(const t of o){const o=JSON.parse(t);if("number"!=typeof o.type)throw new Error("Invalid payload.");switch(o.type){case N.Invocation:this.U(o);break;case N.StreamItem:this.Be(o);break;case N.Completion:this.Xe(o);break;case N.Ping:case N.Close:break;case N.Ack:this.Je(o);break;case N.Sequence:this.ze(o);break;default:n.log(e.Information,"Unknown message type '"+o.type+"' ignored.");continue}i.push(o)}return i}writeMessage(t){return A.write(JSON.stringify(t))}U(t){this.Ve(t.target,"Invalid payload for Invocation message."),void 0!==t.invocationId&&this.Ve(t.invocationId,"Invalid payload for Invocation message.")}Be(t){if(this.Ve(t.invocationId,"Invalid payload for StreamItem message."),void 0===t.item)throw new Error("Invalid payload for StreamItem message.")}Xe(t){if(t.result&&t.error)throw new Error("Invalid payload for Completion message.");!t.result&&t.error&&this.Ve(t.error,"Invalid payload for Completion message."),this.Ve(t.invocationId,"Invalid payload for Completion message.")}Je(t){if("number"!=typeof t.sequenceId)throw new Error("Invalid SequenceId for Ack message.")}ze(t){if("number"!=typeof t.sequenceId)throw new Error("Invalid SequenceId for Sequence message.")}Ve(t,e){if("string"!=typeof t||""===t)throw new Error(e)}}const G={trace:e.Trace,debug:e.Debug,info:e.Information,information:e.Information,warn:e.Warning,warning:e.Warning,error:e.Error,critical:e.Critical,none:e.None};class tt{configureLogging(t){if(m.isRequired(t,"logging"),void 0!==t.log)this.logger=t;else if("string"==typeof t){const e=function(t){const e=G[t.toLowerCase()];if(void 0!==e)return e;throw new Error(`Unknown log level: ${t}`)}(t);this.logger=new w(e)}else this.logger=new w(t);return this}withUrl(t,e){return m.isRequired(t,"url"),m.isNotEmpty(t,"url"),this.url=t,this.httpConnectionOptions="object"==typeof e?{...this.httpConnectionOptions,...e}:{...this.httpConnectionOptions,transport:e},this}withHubProtocol(t){return m.isRequired(t,"protocol"),this.protocol=t,this}withAutomaticReconnect(t){if(this.reconnectPolicy)throw new Error("A reconnectPolicy has already been set.");return t?Array.isArray(t)?this.reconnectPolicy=new z(t):this.reconnectPolicy=t:this.reconnectPolicy=new z,this}withServerTimeout(t){return m.isRequired(t,"milliseconds"),this.Ke=t,this}withKeepAliveInterval(t){return m.isRequired(t,"milliseconds"),this.Ge=t,this}withStatefulReconnect(t){return void 0===this.httpConnectionOptions&&(this.httpConnectionOptions={}),this.httpConnectionOptions.He=!0,this.Y=null==t?void 0:t.bufferSize,this}build(){const t=this.httpConnectionOptions||{};if(void 0===t.logger&&(t.logger=this.logger),!this.url)throw new Error("The 'HubConnectionBuilder.withUrl' method must be called before building the connection.");const e=new J(this.url,t);return O.create(e,this.logger||p.instance,this.protocol||new K,this.reconnectPolicy,this.Ke,this.Ge,this.Y)}}return Uint8Array.prototype.indexOf||Object.defineProperty(Uint8Array.prototype,"indexOf",{value:Array.prototype.indexOf,writable:!0}),Uint8Array.prototype.slice||Object.defineProperty(Uint8Array.prototype,"slice",{value:function(t,e){return new Uint8Array(Array.prototype.slice.call(this,t,e))},writable:!0}),Uint8Array.prototype.forEach||Object.defineProperty(Uint8Array.prototype,"forEach",{value:Array.prototype.forEach,writable:!0}),n})(),"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.signalR=e():t.signalR=e(),$.extend($t,{t1:"Eingabe erforderlich",t2:"Bitte überprüfen Sie Ihre Eingaben im Formular.",b0:"Erstellt",b1:"Zuletzt geändert",b2:"von",t12:"Der Server hat einen Fehler zurückgegeben. Bitte versuchen Sie es erneut.",t17:"Eine Email mit einem Aktivierungs-Link wurde an deine Adresse versandt.",t18:"Ein Account mit deinem Namen existiert bereits. Dennoch erstellen?",t19:"Einträge sind entweder unngültig oder zu kurz.",t20:"Der Server hat einen Fehler gemeldet. Bitte versuch es erneut.",t21:"Der Zugang wurde nicht gefunden.",t30a:"Als erledigt markieren.",t30b:"Als unerledigt markieren.",t55:"Ein Email mit einem Aktivierungs-Link wurde an Ihre Adresse versandt.",t56:"Ein Zugang für diesen Namen besteht bereits. Trotzdem erstellen?",t57:"Ein bestehender Zugang wurde für diese Serie registriert.",t60:"Bitte geben Sie Email-Adresse an, die Sie hier hinterlegt haben.",t61:"Ihr Passwort wurde erfolgreich versandt.",t62:"Die angegebene Email-Adresse stimmt nicht mit der hier hinterlegten überein.",ov:"Persönliche Übersicht"});var $v={}; +var t,e,$t={lng:"de-DE",dn:["So","Mo","Di","Mi","Do","Fr","Sa"],mn:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],ma:["Jan","Feb","Mrz","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],datepattern:"(0[1-9]|1[0-9]|2[0-9]|3[01]).(0[1-9]|1[012]).[0-9]{4}",datetimepattern:"(0[1-9]|1[0-9]|2[0-9]|3[01]).(0[1-9]|1[012]).[0-9]{4}\\s([0-5][0-9]):([0-5][0-9])",dateplaceholder:"dd.MM.yyyy",datetimeplaceholder:"dd.MM.yyyy HH:mm",dateformat:"dd.MM.yyyy",datetimeformat:"dd.MM.yyyy HH:mm",f1:"Der Server hat einen Fehler gemeldet: \n",f2:"Bitte versuchen Sie es erneut.",m0:"Diese Internet-Seite benötigt einen html5-kompatiblen Browser.",m0b:"Unterstützt werden bspw: Internet Explorer ab Version 10, Firefox ab Version 31, Chrome ab Version 31, Safari ab Version 7, Opera ab Version 27",m1:"Dieser Datensatz ist momentan von jemand anderem zur Bearbeitung gesperrt.",m2:"Diese Funktion ist zur Zeit nicht verfügbar",t1:"Eingabe erforderlich.",t2:"Eingabe ist nicht erforderlich.",true:"Ja",false:"Nein",alert:"Hinweis",confirm:"Bestätigen",open:"Öffnen","not implemented":"Diese Funktion in zur Zeit noch nicht verfügbar.",l0:"Anmeldung",l1:"Email / Anmeldename",l2:"Email-Adresse / Anmeldename",l3:"Passwort",l4:"Benutzer",l5:"Wird vom System ermittelt...",l6:"Anmelden",l7:"Passwort vergessen?",l7a:'Die "Passwort vergessen"-Funktion läuft in zwei Schritten ab:\n \nIm ersten Schritt wird eine SMS mit einem Code an die hinterlegte Mobilfunk-Nummer versandt.\nIm zweiten Schritt geben Sie bitte diesen Code in das Formular ein und übermitteln es erneut.\n \nIn beiden Schritten wird aus Sicherheitsgründen kein Fehler angezeigt und auch dann ein erfolgreicher Versand bestätigt, wenn die Kombination aus Email-Adresse und Nachname nicht gefunden wurde und/oder der code falsch ist.',l8:"Keinen Account?",l9:"Anmeldenamen der Email-Adresse wurde nicht erkannt.",l10:"Nachname",l11:"Email-Adresse",l12:"Passwort zusenden",l13:"Das Passwort wurde erfolgreich verschickt",l14:"Das Passwort konnte nicht verschickt werden",l15:"Sie sind nicht berechtigt, diese Funktion auszuführen.",l16:"Sie müssen zunächst einen Account angeben.",l17:"Die Kombination aus Anmeldenamen und Passwort konnte nicht bestätigt werden.",l18:"Es gibt ein Problem mit dem Formular.\nEs kann momentan nicht verarbeitet und versendet werden.",name:"Name",submit:"Senden",cancel:"Abbrechen",noop:"Diese Funktion is noch nicht verfügar."};t=self,e=()=>(()=>{var t={d:(e,n)=>{for(var i in n)t.o(n,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:n[i]})}};t.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),t.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),t.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"t",{value:!0})};var e,n={};t.r(n),t.d(n,{AbortError:()=>r,DefaultHttpClient:()=>D,HttpClient:()=>h,HttpError:()=>i,HttpResponse:()=>d,HttpTransportType:()=>U,HubConnection:()=>O,HubConnectionBuilder:()=>tt,HubConnectionState:()=>j,JsonHubProtocol:()=>K,LogLevel:()=>e,MessageType:()=>N,NullLogger:()=>p,Subject:()=>q,TimeoutError:()=>o,TransferFormat:()=>B,VERSION:()=>f});class i extends Error{constructor(t,e){const n=new.target.prototype;super(`${t}: Status code '${e}'`),this.statusCode=e,this.__proto__=n}}class o extends Error{constructor(t="A timeout occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class r extends Error{constructor(t="An abort occurred."){const e=new.target.prototype;super(t),this.__proto__=e}}class s extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.transport=e,this.errorType="UnsupportedTransportError",this.__proto__=n}}class a extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.transport=e,this.errorType="DisabledTransportError",this.__proto__=n}}class c extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.transport=e,this.errorType="FailedToStartTransportError",this.__proto__=n}}class l extends Error{constructor(t){const e=new.target.prototype;super(t),this.errorType="FailedToNegotiateWithServerError",this.__proto__=e}}class u extends Error{constructor(t,e){const n=new.target.prototype;super(t),this.innerErrors=e,this.__proto__=n}}class d{constructor(t,e,n){this.statusCode=t,this.statusText=e,this.content=n}}class h{get(t,e){return this.send({...e,method:"GET",url:t})}post(t,e){return this.send({...e,method:"POST",url:t})}delete(t,e){return this.send({...e,method:"DELETE",url:t})}getCookieString(t){return""}}!function(t){t[t.Trace=0]="Trace",t[t.Debug=1]="Debug",t[t.Information=2]="Information",t[t.Warning=3]="Warning",t[t.Error=4]="Error",t[t.Critical=5]="Critical",t[t.None=6]="None"}(e||(e={}));class p{constructor(){}log(t,e){}}p.instance=new p;const f="10.0.0";class m{static isRequired(t,e){if(null==t)throw new Error(`The '${e}' argument is required.`)}static isNotEmpty(t,e){if(!t||t.match(/^\s*$/))throw new Error(`The '${e}' argument should not be empty.`)}static isIn(t,e,n){if(!(t in e))throw new Error(`Unknown ${n} value: ${t}.`)}}class g{static get isBrowser(){return!g.isNode&&"object"==typeof window&&"object"==typeof window.document}static get isWebWorker(){return!g.isNode&&"object"==typeof self&&"importScripts"in self}static get isReactNative(){return!g.isNode&&"object"==typeof window&&void 0===window.document}static get isNode(){return"undefined"!=typeof process&&process.release&&"node"===process.release.name}}function y(t,e){let n="";return v(t)?(n=`Binary data of length ${t.byteLength}`,e&&(n+=`. Content: '${function(t){const e=new Uint8Array(t);let n="";return e.forEach((t=>{n+=`0x${t<16?"0":""}${t.toString(16)} `})),n.substring(0,n.length-1)}(t)}'`)):"string"==typeof t&&(n=`String data of length ${t.length}`,e&&(n+=`. Content: '${t}'`)),n}function v(t){return t&&"undefined"!=typeof ArrayBuffer&&(t instanceof ArrayBuffer||t.constructor&&"ArrayBuffer"===t.constructor.name)}async function b(t,n,i,o,r,s){const a={},[c,l]=C();a[c]=l,t.log(e.Trace,`(${n} transport) sending data. ${y(r,s.logMessageContent)}.`);const u=v(r)?"arraybuffer":"text",d=await i.post(o,{content:r,headers:{...a,...s.headers},responseType:u,timeout:s.timeout,withCredentials:s.withCredentials});t.log(e.Trace,`(${n} transport) request complete. Response status: ${d.statusCode}.`)}class ${constructor(t,e){this.i=t,this.h=e}dispose(){const t=this.i.observers.indexOf(this.h);t>-1&&this.i.observers.splice(t,1),0===this.i.observers.length&&this.i.cancelCallback&&this.i.cancelCallback().catch((t=>{}))}}class w{constructor(t){this.l=t,this.out=console}log(t,n){if(t>=this.l){const i=`[${(new Date).toISOString()}] ${e[t]}: ${n}`;switch(t){case e.Critical:case e.Error:this.out.error(i);break;case e.Warning:this.out.warn(i);break;case e.Information:this.out.info(i);break;default:this.out.log(i)}}}}function C(){let t="X-SignalR-User-Agent";return g.isNode&&(t="User-Agent"),[t,S(f,_(),g.isNode?"NodeJS":"Browser",x())]}function S(t,e,n,i){let o="Microsoft SignalR/";const r=t.split(".");return o+=`${r[0]}.${r[1]}`,o+=` (${t}; `,o+=e&&""!==e?`${e}; `:"Unknown OS; ",o+=`${n}`,o+=i?`; ${i}`:"; Unknown Runtime Version",o+=")",o}function _(){if(!g.isNode)return"";switch(process.platform){case"win32":return"Windows NT";case"darwin":return"macOS";case"linux":return"Linux";default:return process.platform}}function x(){if(g.isNode)return process.versions.node}function T(t){return t.stack?t.stack:t.message?t.message:`${t}`}class E extends h{constructor(e){if(super(),this.u=e,"undefined"==typeof fetch||g.isNode){const t=require;this.p=new(t("tough-cookie").CookieJar),"undefined"==typeof fetch?this.m=t("node-fetch"):this.m=fetch,this.m=t("fetch-cookie")(this.m,this.p)}else this.m=fetch.bind(function(){if("undefined"!=typeof globalThis)return globalThis;if("undefined"!=typeof self)return self;if("undefined"!=typeof window)return window;if(void 0!==t.g)return t.g;throw new Error("could not find global")}());if("undefined"==typeof AbortController){const t=require;this.v=t("abort-controller")}else this.v=AbortController}async send(t){if(t.abortSignal&&t.abortSignal.aborted)throw new r;if(!t.method)throw new Error("No method defined.");if(!t.url)throw new Error("No url defined.");const n=new this.v;let s;t.abortSignal&&(t.abortSignal.onabort=()=>{n.abort(),s=new r});let a,c=null;if(t.timeout){const i=t.timeout;c=setTimeout((()=>{n.abort(),this.u.log(e.Warning,"Timeout from HTTP request."),s=new o}),i)}""===t.content&&(t.content=void 0),t.content&&(t.headers=t.headers||{},v(t.content)?t.headers["Content-Type"]="application/octet-stream":t.headers["Content-Type"]="text/plain;charset=UTF-8");try{a=await this.m(t.url,{body:t.content,cache:"no-cache",credentials:!0===t.withCredentials?"include":"same-origin",headers:{"X-Requested-With":"XMLHttpRequest",...t.headers},method:t.method,mode:"cors",redirect:"follow",signal:n.signal})}catch(t){if(s)throw s;throw this.u.log(e.Warning,`Error from HTTP request. ${t}.`),t}finally{c&&clearTimeout(c),t.abortSignal&&(t.abortSignal.onabort=null)}if(!a.ok){const t=await k(a,"text");throw new i(t||a.statusText,a.status)}const l=k(a,t.responseType),u=await l;return new d(a.status,a.statusText,u)}getCookieString(t){let e="";return g.isNode&&this.p&&this.p.getCookies(t,((t,n)=>e=n.join("; "))),e}}function k(t,e){let n;switch(e){case"arraybuffer":n=t.arrayBuffer();break;case"text":default:n=t.text();break;case"blob":case"document":case"json":throw new Error(`${e} is not supported.`)}return n}class I extends h{constructor(t){super(),this.u=t}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?new Promise(((n,s)=>{const a=new XMLHttpRequest;a.open(t.method,t.url,!0),a.withCredentials=void 0===t.withCredentials||t.withCredentials,a.setRequestHeader("X-Requested-With","XMLHttpRequest"),""===t.content&&(t.content=void 0),t.content&&(v(t.content)?a.setRequestHeader("Content-Type","application/octet-stream"):a.setRequestHeader("Content-Type","text/plain;charset=UTF-8"));const c=t.headers;c&&Object.keys(c).forEach((t=>{a.setRequestHeader(t,c[t])})),t.responseType&&(a.responseType=t.responseType),t.abortSignal&&(t.abortSignal.onabort=()=>{a.abort(),s(new r)}),t.timeout&&(a.timeout=t.timeout),a.onload=()=>{t.abortSignal&&(t.abortSignal.onabort=null),a.status>=200&&a.status<300?n(new d(a.status,a.statusText,a.response||a.responseText)):s(new i(a.response||a.responseText||a.statusText,a.status))},a.onerror=()=>{this.u.log(e.Warning,`Error from HTTP request. ${a.status}: ${a.statusText}.`),s(new i(a.statusText,a.status))},a.ontimeout=()=>{this.u.log(e.Warning,"Timeout from HTTP request."),s(new o)},a.send(t.content)})):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}}class D extends h{constructor(t){if(super(),"undefined"!=typeof fetch||g.isNode)this.$=new E(t);else{if("undefined"==typeof XMLHttpRequest)throw new Error("No usable HttpClient found.");this.$=new I(t)}}send(t){return t.abortSignal&&t.abortSignal.aborted?Promise.reject(new r):t.method?t.url?this.$.send(t):Promise.reject(new Error("No url defined.")):Promise.reject(new Error("No method defined."))}getCookieString(t){return this.$.getCookieString(t)}}class A{static write(t){return`${t}${A.RecordSeparator}`}static parse(t){if(t[t.length-1]!==A.RecordSeparator)throw new Error("Message is incomplete.");const e=t.split(A.RecordSeparator);return e.pop(),e}}A.RecordSeparatorCode=30,A.RecordSeparator=String.fromCharCode(A.RecordSeparatorCode);class P{writeHandshakeRequest(t){return A.write(JSON.stringify(t))}parseHandshakeResponse(t){let e,n;if(v(t)){const i=new Uint8Array(t),o=i.indexOf(A.RecordSeparatorCode);if(-1===o)throw new Error("Message is incomplete.");const r=o+1;e=String.fromCharCode.apply(null,Array.prototype.slice.call(i.slice(0,r))),n=i.byteLength>r?i.slice(r).buffer:null}else{const i=t,o=i.indexOf(A.RecordSeparator);if(-1===o)throw new Error("Message is incomplete.");const r=o+1;e=i.substring(0,r),n=i.length>r?i.substring(r):null}const i=A.parse(e),o=JSON.parse(i[0]);if(o.type)throw new Error("Expected a handshake response from the server.");return[n,o]}}var N,j;!function(t){t[t.Invocation=1]="Invocation",t[t.StreamItem=2]="StreamItem",t[t.Completion=3]="Completion",t[t.StreamInvocation=4]="StreamInvocation",t[t.CancelInvocation=5]="CancelInvocation",t[t.Ping=6]="Ping",t[t.Close=7]="Close",t[t.Ack=8]="Ack",t[t.Sequence=9]="Sequence"}(N||(N={}));class q{constructor(){this.observers=[]}next(t){for(const e of this.observers)e.next(t)}error(t){for(const e of this.observers)e.error&&e.error(t)}complete(){for(const t of this.observers)t.complete&&t.complete()}subscribe(t){return this.observers.push(t),new $(this,t)}}class M{constructor(t,e,n){this.C=1e5,this.S=[],this.k=0,this.P=!1,this.T=1,this.I=0,this._=0,this.H=!1,this.D=t,this.R=e,this.C=n}async A(t){const e=this.D.writeMessage(t);let n=Promise.resolve();if(this.U(t)){this.k++;let t=()=>{},i=()=>{};v(e)?this._+=e.byteLength:this._+=e.length,this._>=this.C&&(n=new Promise(((e,n)=>{t=e,i=n}))),this.S.push(new R(e,this.k,t,i))}try{this.H||await this.R.send(e)}catch{this.L()}await n}N(t){let e=-1;for(let n=0;nthis.T?this.R.stop(new Error("Sequence ID greater than amount of messages we've received.")):this.T=t.sequenceId}L(){this.H=!0,this.P=!0}async B(){const t=0!==this.S.length?this.S[0].q:this.k+1;await this.R.send(this.D.writeMessage({type:N.Sequence,sequenceId:t}));const e=this.S;for(const t of e)await this.R.send(t.M);this.H=!1}X(t){null!=t||(t=new Error("Unable to reconnect to server."));for(const e of this.S)e.J(t)}U(t){switch(t.type){case N.Invocation:case N.StreamItem:case N.Completion:case N.StreamInvocation:case N.CancelInvocation:return!0;case N.Close:case N.Sequence:case N.Ping:case N.Ack:return!1}}O(){void 0===this.V&&(this.V=setTimeout((async()=>{try{this.H||await this.R.send(this.D.writeMessage({type:N.Ack,sequenceId:this.I}))}catch{}clearTimeout(this.V),this.V=void 0}),1e3))}}class R{constructor(t,e,n,i){this.M=t,this.q=e,this.j=n,this.J=i}}!function(t){t.Disconnected="Disconnected",t.Connecting="Connecting",t.Connected="Connected",t.Disconnecting="Disconnecting",t.Reconnecting="Reconnecting"}(j||(j={}));class O{static create(t,e,n,i,o,r,s){return new O(t,e,n,i,o,r,s)}constructor(t,n,i,o,r,s,a){this.K=0,this.G=()=>{this.u.log(e.Warning,"The page is being frozen, this will likely lead to the connection being closed and messages being lost. For more information see the docs at https://learn.microsoft.com/aspnet/core/signalr/javascript-client#bsleep")},m.isRequired(t,"connection"),m.isRequired(n,"logger"),m.isRequired(i,"protocol"),this.serverTimeoutInMilliseconds=null!=r?r:3e4,this.keepAliveIntervalInMilliseconds=null!=s?s:15e3,this.Y=null!=a?a:1e5,this.u=n,this.D=i,this.connection=t,this.Z=o,this.tt=new P,this.connection.onreceive=t=>this.et(t),this.connection.onclose=t=>this.st(t),this.it={},this.nt={},this.rt=[],this.ot=[],this.ht=[],this.ct=0,this.lt=!1,this.ut=j.Disconnected,this.dt=!1,this.ft=this.D.writeMessage({type:N.Ping})}get state(){return this.ut}get connectionId(){return this.connection&&this.connection.connectionId||null}get baseUrl(){return this.connection.baseUrl||""}set baseUrl(t){if(this.ut!==j.Disconnected&&this.ut!==j.Reconnecting)throw new Error("The HubConnection must be in the Disconnected or Reconnecting state to change the url.");if(!t)throw new Error("The HubConnection url must be a valid url.");this.connection.baseUrl=t}start(){return this.wt=this.gt(),this.wt}async gt(){if(this.ut!==j.Disconnected)return Promise.reject(new Error("Cannot start a HubConnection that is not in the 'Disconnected' state."));this.ut=j.Connecting,this.u.log(e.Debug,"Starting HubConnection.");try{await this.yt(),g.isBrowser&&window.document.addEventListener("freeze",this.G),this.ut=j.Connected,this.dt=!0,this.u.log(e.Debug,"HubConnection connected successfully.")}catch(t){return this.ut=j.Disconnected,this.u.log(e.Debug,`HubConnection failed to start successfully because of error '${t}'.`),Promise.reject(t)}}async yt(){this.vt=void 0,this.lt=!1;const t=new Promise(((t,e)=>{this.bt=t,this.Et=e}));await this.connection.start(this.D.transferFormat);try{let n=this.D.version;this.connection.features.reconnect||(n=1);const i={protocol:this.D.name,version:n};if(this.u.log(e.Debug,"Sending handshake request."),await this.$t(this.tt.writeHandshakeRequest(i)),this.u.log(e.Information,`Using HubProtocol '${this.D.name}'.`),this.Ct(),this.St(),this.kt(),await t,this.vt)throw this.vt;!!this.connection.features.reconnect&&(this.Pt=new M(this.D,this.connection,this.Y),this.connection.features.disconnected=this.Pt.L.bind(this.Pt),this.connection.features.resend=()=>{if(this.Pt)return this.Pt.B()}),this.connection.features.inherentKeepAlive||await this.$t(this.ft)}catch(t){throw this.u.log(e.Debug,`Hub handshake failed with error '${t}' during start(). Stopping HubConnection.`),this.Ct(),this.Tt(),await this.connection.stop(t),t}}async stop(){const t=this.wt;this.connection.features.reconnect=!1,this.It=this._t(),await this.It;try{await t}catch(t){}}_t(t){if(this.ut===j.Disconnected)return this.u.log(e.Debug,`Call to HubConnection.stop(${t}) ignored because it is already in the disconnected state.`),Promise.resolve();if(this.ut===j.Disconnecting)return this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.It;const n=this.ut;return this.ut=j.Disconnecting,this.u.log(e.Debug,"Stopping HubConnection."),this.Ht?(this.u.log(e.Debug,"Connection stopped during reconnect delay. Done reconnecting."),clearTimeout(this.Ht),this.Ht=void 0,this.Dt(),Promise.resolve()):(n===j.Connected&&this.Rt(),this.Ct(),this.Tt(),this.vt=t||new r("The connection was stopped before the hub handshake could complete."),this.connection.stop(t))}async Rt(){try{await this.xt(this.At())}catch{}}stream(t,...e){const[n,i]=this.Ut(e),o=this.Lt(t,e,i);let r;const s=new q;return s.cancelCallback=()=>{const t=this.Nt(o.invocationId);return delete this.it[o.invocationId],r.then((()=>this.xt(t)))},this.it[o.invocationId]=(t,e)=>{e?s.error(e):t&&(t.type===N.Completion?t.error?s.error(new Error(t.error)):s.complete():s.next(t.item))},r=this.xt(o).catch((t=>{s.error(t),delete this.it[o.invocationId]})),this.qt(n,r),s}$t(t){return this.kt(),this.connection.send(t)}xt(t){return this.Pt?this.Pt.A(t):this.$t(this.D.writeMessage(t))}send(t,...e){const[n,i]=this.Ut(e),o=this.xt(this.Mt(t,e,!0,i));return this.qt(n,o),o}invoke(t,...e){const[n,i]=this.Ut(e),o=this.Mt(t,e,!1,i);return new Promise(((t,e)=>{this.it[o.invocationId]=(n,i)=>{i?e(i):n&&(n.type===N.Completion?n.error?e(new Error(n.error)):t(n.result):e(new Error(`Unexpected message type: ${n.type}`)))};const i=this.xt(o).catch((t=>{e(t),delete this.it[o.invocationId]}));this.qt(n,i)}))}on(t,e){t&&e&&(t=t.toLowerCase(),this.nt[t]||(this.nt[t]=[]),-1===this.nt[t].indexOf(e)&&this.nt[t].push(e))}off(t,e){if(!t)return;t=t.toLowerCase();const n=this.nt[t];if(n)if(e){const i=n.indexOf(e);-1!==i&&(n.splice(i,1),0===n.length&&delete this.nt[t])}else delete this.nt[t]}onclose(t){t&&this.rt.push(t)}onreconnecting(t){t&&this.ot.push(t)}onreconnected(t){t&&this.ht.push(t)}et(t){if(this.Ct(),this.lt||(t=this.jt(t),this.lt=!0),t){const n=this.D.parseMessages(t,this.u);for(const i of n)if(!this.Pt||this.Pt.W(i))switch(i.type){case N.Invocation:this.Wt(i).catch((t=>{this.u.log(e.Error,`Invoke client method threw error: ${T(t)}`)}));break;case N.StreamItem:case N.Completion:{const n=this.it[i.invocationId];if(n){i.type===N.Completion&&delete this.it[i.invocationId];try{n(i)}catch(t){this.u.log(e.Error,`Stream callback threw error: ${T(t)}`)}}break}case N.Ping:break;case N.Close:{this.u.log(e.Information,"Close message received from server.");const t=i.error?new Error("Server returned an error on close: "+i.error):void 0;!0===i.allowReconnect?this.connection.stop(t):this.It=this._t(t);break}case N.Ack:this.Pt&&this.Pt.N(i);break;case N.Sequence:this.Pt&&this.Pt.F(i);break;default:this.u.log(e.Warning,`Invalid message type: ${i.type}.`)}}this.St()}jt(t){let n,i;try{[i,n]=this.tt.parseHandshakeResponse(t)}catch(t){const n="Error parsing handshake response: "+t;this.u.log(e.Error,n);const i=new Error(n);throw this.Et(i),i}if(n.error){const t="Server returned handshake error: "+n.error;this.u.log(e.Error,t);const i=new Error(t);throw this.Et(i),i}return this.u.log(e.Debug,"Server handshake complete."),this.bt(),i}kt(){this.connection.features.inherentKeepAlive||(this.K=(new Date).getTime()+this.keepAliveIntervalInMilliseconds,this.Tt())}St(){if(!this.connection.features||!this.connection.features.inherentKeepAlive){this.Ot=setTimeout((()=>this.serverTimeout()),this.serverTimeoutInMilliseconds);let t=this.K-(new Date).getTime();if(t<0)return void(this.ut===j.Connected&&this.Ft());void 0===this.Bt&&(t<0&&(t=0),this.Bt=setTimeout((async()=>{this.ut===j.Connected&&await this.Ft()}),t))}}serverTimeout(){this.connection.stop(new Error("Server timeout elapsed without receiving a message from the server."))}async Wt(t){const n=t.target.toLowerCase(),i=this.nt[n];if(!i)return this.u.log(e.Warning,`No client method with the name '${n}' found.`),void(t.invocationId&&(this.u.log(e.Warning,`No result given for '${n}' method and invocation ID '${t.invocationId}'.`),await this.xt(this.Xt(t.invocationId,"Client didn't provide a result.",null))));const o=i.slice(),r=!!t.invocationId;let s,a,c;for(const i of o)try{const o=s;s=await i.apply(this,t.arguments),r&&s&&o&&(this.u.log(e.Error,`Multiple results provided for '${n}'. Sending error to server.`),c=this.Xt(t.invocationId,"Client provided multiple results.",null)),a=void 0}catch(t){a=t,this.u.log(e.Error,`A callback for the method '${n}' threw error '${t}'.`)}c?await this.xt(c):r?(a?c=this.Xt(t.invocationId,`${a}`,null):void 0!==s?c=this.Xt(t.invocationId,null,s):(this.u.log(e.Warning,`No result given for '${n}' method and invocation ID '${t.invocationId}'.`),c=this.Xt(t.invocationId,"Client didn't provide a result.",null)),await this.xt(c)):s&&this.u.log(e.Error,`Result given for '${n}' method but server is not expecting a result.`)}st(t){this.u.log(e.Debug,`HubConnection.connectionClosed(${t}) called while in state ${this.ut}.`),this.vt=this.vt||t||new r("The underlying connection was closed before the hub handshake could complete."),this.bt&&this.bt(),this.Jt(t||new Error("Invocation canceled due to the underlying connection being closed.")),this.Ct(),this.Tt(),this.ut===j.Disconnecting?this.Dt(t):this.ut===j.Connected&&this.Z?this.zt(t):this.ut===j.Connected&&this.Dt(t)}Dt(t){if(this.dt){this.ut=j.Disconnected,this.dt=!1,this.Pt&&(this.Pt.X(null!=t?t:new Error("Connection closed.")),this.Pt=void 0),g.isBrowser&&window.document.removeEventListener("freeze",this.G);try{this.rt.forEach((e=>e.apply(this,[t])))}catch(n){this.u.log(e.Error,`An onclose callback called with error '${t}' threw error '${n}'.`)}}}async zt(t){const n=Date.now();let i=0,o=void 0!==t?t:new Error("Attempting to reconnect due to a unknown error."),r=this.Vt(i,0,o);if(null===r)return this.u.log(e.Debug,"Connection not reconnecting because the IRetryPolicy returned null on the first reconnect attempt."),void this.Dt(t);if(this.ut=j.Reconnecting,t?this.u.log(e.Information,`Connection reconnecting because of error '${t}'.`):this.u.log(e.Information,"Connection reconnecting."),0!==this.ot.length){try{this.ot.forEach((e=>e.apply(this,[t])))}catch(n){this.u.log(e.Error,`An onreconnecting callback called with error '${t}' threw error '${n}'.`)}if(this.ut!==j.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state in onreconnecting callback. Done reconnecting.")}for(;null!==r;){if(this.u.log(e.Information,`Reconnect attempt number ${i+1} will start in ${r} ms.`),await new Promise((t=>{this.Ht=setTimeout(t,r)})),this.Ht=void 0,this.ut!==j.Reconnecting)return void this.u.log(e.Debug,"Connection left the reconnecting state during reconnect delay. Done reconnecting.");try{if(await this.yt(),this.ut=j.Connected,this.u.log(e.Information,"HubConnection reconnected successfully."),0!==this.ht.length)try{this.ht.forEach((t=>t.apply(this,[this.connection.connectionId])))}catch(t){this.u.log(e.Error,`An onreconnected callback called with connectionId '${this.connection.connectionId}; threw error '${t}'.`)}return}catch(t){if(this.u.log(e.Information,`Reconnect attempt failed because of error '${t}'.`),this.ut!==j.Reconnecting)return this.u.log(e.Debug,`Connection moved to the '${this.ut}' from the reconnecting state during reconnect attempt. Done reconnecting.`),void(this.ut===j.Disconnecting&&this.Dt());i++,o=t instanceof Error?t:new Error(t.toString()),r=this.Vt(i,Date.now()-n,o)}}this.u.log(e.Information,`Reconnect retries have been exhausted after ${Date.now()-n} ms and ${i} failed attempts. Connection disconnecting.`),this.Dt()}Vt(t,n,i){try{return this.Z.nextRetryDelayInMilliseconds({elapsedMilliseconds:n,previousRetryCount:t,retryReason:i})}catch(i){return this.u.log(e.Error,`IRetryPolicy.nextRetryDelayInMilliseconds(${t}, ${n}) threw error '${i}'.`),null}}Jt(t){const n=this.it;this.it={},Object.keys(n).forEach((i=>{const o=n[i];try{o(null,t)}catch(n){this.u.log(e.Error,`Stream 'error' callback called with '${t}' threw error: ${T(n)}`)}}))}Tt(){this.Bt&&(clearTimeout(this.Bt),this.Bt=void 0)}Ct(){this.Ot&&clearTimeout(this.Ot)}Mt(t,e,n,i){if(n)return 0!==i.length?{target:t,arguments:e,streamIds:i,type:N.Invocation}:{target:t,arguments:e,type:N.Invocation};{const n=this.ct;return this.ct++,0!==i.length?{target:t,arguments:e,invocationId:n.toString(),streamIds:i,type:N.Invocation}:{target:t,arguments:e,invocationId:n.toString(),type:N.Invocation}}}qt(t,e){if(0!==t.length){e||(e=Promise.resolve());for(const n in t)t[n].subscribe({complete:()=>{e=e.then((()=>this.xt(this.Xt(n))))},error:t=>{let i;i=t instanceof Error?t.message:t&&t.toString?t.toString():"Unknown error",e=e.then((()=>this.xt(this.Xt(n,i))))},next:t=>{e=e.then((()=>this.xt(this.Kt(n,t))))}})}}Ut(t){const e=[],n=[];for(let i=0;i0)&&(e=!1,this.te=await this.Zt()),this.ee(t);const n=await this.Yt.send(t);return e&&401===n.statusCode&&this.Zt?(this.te=await this.Zt(),this.ee(t),await this.Yt.send(t)):n}ee(t){t.headers||(t.headers={}),this.te?t.headers[L.Authorization]=`Bearer ${this.te}`:this.Zt&&t.headers[L.Authorization]&&delete t.headers[L.Authorization]}getCookieString(t){return this.Yt.getCookieString(t)}}var U,B;!function(t){t[t.None=0]="None",t[t.WebSockets=1]="WebSockets",t[t.ServerSentEvents=2]="ServerSentEvents",t[t.LongPolling=4]="LongPolling"}(U||(U={})),function(t){t[t.Text=1]="Text",t[t.Binary=2]="Binary"}(B||(B={}));class W{constructor(){this.se=!1,this.onabort=null}abort(){this.se||(this.se=!0,this.onabort&&this.onabort())}get signal(){return this}get aborted(){return this.se}}class X{get pollAborted(){return this.ie.aborted}constructor(t,e,n){this.$=t,this.u=e,this.ie=new W,this.ne=n,this.re=!1,this.onreceive=null,this.onclose=null}async connect(t,n){if(m.isRequired(t,"url"),m.isRequired(n,"transferFormat"),m.isIn(n,B,"transferFormat"),this.oe=t,this.u.log(e.Trace,"(LongPolling transport) Connecting."),n===B.Binary&&"undefined"!=typeof XMLHttpRequest&&"string"!=typeof(new XMLHttpRequest).responseType)throw new Error("Binary protocols over XmlHttpRequest not implementing advanced features are not supported.");const[o,r]=C(),s={[o]:r,...this.ne.headers},a={abortSignal:this.ie.signal,headers:s,timeout:1e5,withCredentials:this.ne.withCredentials};n===B.Binary&&(a.responseType="arraybuffer");const c=`${t}&_=${Date.now()}`;this.u.log(e.Trace,`(LongPolling transport) polling: ${c}.`);const l=await this.$.get(c,a);200!==l.statusCode?(this.u.log(e.Error,`(LongPolling transport) Unexpected response code: ${l.statusCode}.`),this.he=new i(l.statusText||"",l.statusCode),this.re=!1):this.re=!0,this.ce=this.ae(this.oe,a)}async ae(t,n){try{for(;this.re;)try{const o=`${t}&_=${Date.now()}`;this.u.log(e.Trace,`(LongPolling transport) polling: ${o}.`);const r=await this.$.get(o,n);204===r.statusCode?(this.u.log(e.Information,"(LongPolling transport) Poll terminated by server."),this.re=!1):200!==r.statusCode?(this.u.log(e.Error,`(LongPolling transport) Unexpected response code: ${r.statusCode}.`),this.he=new i(r.statusText||"",r.statusCode),this.re=!1):r.content?(this.u.log(e.Trace,`(LongPolling transport) data received. ${y(r.content,this.ne.logMessageContent)}.`),this.onreceive&&this.onreceive(r.content)):this.u.log(e.Trace,"(LongPolling transport) Poll timed out, reissuing.")}catch(t){this.re?t instanceof o?this.u.log(e.Trace,"(LongPolling transport) Poll timed out, reissuing."):(this.he=t,this.re=!1):this.u.log(e.Trace,`(LongPolling transport) Poll errored after shutdown: ${t.message}`)}}finally{this.u.log(e.Trace,"(LongPolling transport) Polling complete."),this.pollAborted||this.le()}}async send(t){return this.re?b(this.u,"LongPolling",this.$,this.oe,t,this.ne):Promise.reject(new Error("Cannot send until the transport is connected"))}async stop(){this.u.log(e.Trace,"(LongPolling transport) Stopping polling."),this.re=!1,this.ie.abort();try{await this.ce,this.u.log(e.Trace,`(LongPolling transport) sending DELETE request to ${this.oe}.`);const t={},[n,o]=C();t[n]=o;const r={headers:{...t,...this.ne.headers},timeout:this.ne.timeout,withCredentials:this.ne.withCredentials};let s;try{await this.$.delete(this.oe,r)}catch(t){s=t}s?s instanceof i&&(404===s.statusCode?this.u.log(e.Trace,"(LongPolling transport) A 404 response was returned from sending a DELETE request."):this.u.log(e.Trace,`(LongPolling transport) Error sending a DELETE request: ${s}`)):this.u.log(e.Trace,"(LongPolling transport) DELETE request accepted.")}finally{this.u.log(e.Trace,"(LongPolling transport) Stop finished."),this.le()}}le(){if(this.onclose){let t="(LongPolling transport) Firing onclose event.";this.he&&(t+=" Error: "+this.he),this.u.log(e.Trace,t),this.onclose(this.he)}}}class V{constructor(t,e,n,i){this.$=t,this.te=e,this.u=n,this.ne=i,this.onreceive=null,this.onclose=null}async connect(t,n){return m.isRequired(t,"url"),m.isRequired(n,"transferFormat"),m.isIn(n,B,"transferFormat"),this.u.log(e.Trace,"(SSE transport) Connecting."),this.oe=t,this.te&&(t+=(t.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(this.te)}`),new Promise(((i,o)=>{let r,s=!1;if(n===B.Text){if(g.isBrowser||g.isWebWorker)r=new this.ne.EventSource(t,{withCredentials:this.ne.withCredentials});else{const e=this.$.getCookieString(t),n={};n.Cookie=e;const[i,o]=C();n[i]=o,r=new this.ne.EventSource(t,{withCredentials:this.ne.withCredentials,headers:{...n,...this.ne.headers}})}try{r.onmessage=t=>{if(this.onreceive)try{this.u.log(e.Trace,`(SSE transport) data received. ${y(t.data,this.ne.logMessageContent)}.`),this.onreceive(t.data)}catch(t){return void this.ue(t)}},r.onerror=t=>{s?this.ue():o(new Error("EventSource failed to connect. The connection could not be found on the server, either the connection ID is not present on the server, or a proxy is refusing/buffering the connection. If you have multiple servers check that sticky sessions are enabled."))},r.onopen=()=>{this.u.log(e.Information,`SSE connected to ${this.oe}`),this.de=r,s=!0,i()}}catch(t){return void o(t)}}else o(new Error("The Server-Sent Events transport only supports the 'Text' transfer format"))}))}async send(t){return this.de?b(this.u,"SSE",this.$,this.oe,t,this.ne):Promise.reject(new Error("Cannot send until the transport is connected"))}stop(){return this.ue(),Promise.resolve()}ue(t){this.de&&(this.de.close(),this.de=void 0,this.onclose&&this.onclose(t))}}class J{constructor(t,e,n,i,o,r){this.u=n,this.Zt=e,this.fe=i,this.pe=o,this.$=t,this.onreceive=null,this.onclose=null,this.we=r}async connect(t,n){let i;return m.isRequired(t,"url"),m.isRequired(n,"transferFormat"),m.isIn(n,B,"transferFormat"),this.u.log(e.Trace,"(WebSockets transport) Connecting."),this.Zt&&(i=await this.Zt()),new Promise(((o,r)=>{let s;t=t.replace(/^http/,"ws");const a=this.$.getCookieString(t);let c=!1;if(g.isNode||g.isReactNative){const e={},[n,o]=C();e[n]=o,i&&(e[L.Authorization]=`Bearer ${i}`),a&&(e[L.Cookie]=a),s=new this.pe(t,void 0,{headers:{...e,...this.we}})}else i&&(t+=(t.indexOf("?")<0?"?":"&")+`access_token=${encodeURIComponent(i)}`);s||(s=new this.pe(t)),n===B.Binary&&(s.binaryType="arraybuffer"),s.onopen=n=>{this.u.log(e.Information,`WebSocket connected to ${t}.`),this.ge=s,c=!0,o()},s.onerror=t=>{let n=null;n="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"There was an error with the transport",this.u.log(e.Information,`(WebSockets transport) ${n}.`)},s.onmessage=t=>{if(this.u.log(e.Trace,`(WebSockets transport) data received. ${y(t.data,this.fe)}.`),this.onreceive)try{this.onreceive(t.data)}catch(t){return void this.ue(t)}},s.onclose=t=>{if(c)this.ue(t);else{let e=null;e="undefined"!=typeof ErrorEvent&&t instanceof ErrorEvent?t.error:"WebSocket failed to connect. The connection could not be found on the server, either the endpoint may not be a SignalR endpoint, the connection ID is not present on the server, or there is a proxy blocking WebSockets. If you have multiple servers check that sticky sessions are enabled.",r(new Error(e))}}}))}send(t){return this.ge&&this.ge.readyState===this.pe.OPEN?(this.u.log(e.Trace,`(WebSockets transport) sending data. ${y(t,this.fe)}.`),this.ge.send(t),Promise.resolve()):Promise.reject("WebSocket is not in the OPEN state")}stop(){return this.ge&&this.ue(void 0),Promise.resolve()}ue(t){this.ge&&(this.ge.onclose=()=>{},this.ge.onmessage=()=>{},this.ge.onerror=()=>{},this.ge.close(),this.ge=void 0),this.u.log(e.Trace,"(WebSockets transport) socket closed."),this.onclose&&(!this.me(t)||!1!==t.wasClean&&1e3===t.code?t instanceof Error?this.onclose(t):this.onclose():this.onclose(new Error(`WebSocket closed with status code: ${t.code} (${t.reason||"no reason given"}).`)))}me(t){return t&&"boolean"==typeof t.wasClean&&"number"==typeof t.code}}class Z{constructor(t,n={}){var i;if(this.ye=()=>{},this.features={},this.ve=1,m.isRequired(t,"url"),this.u=void 0===(i=n.logger)?new w(e.Information):null===i?p.instance:void 0!==i.log?i:new w(i),this.baseUrl=this.be(t),(n=n||{}).logMessageContent=void 0!==n.logMessageContent&&n.logMessageContent,"boolean"!=typeof n.withCredentials&&void 0!==n.withCredentials)throw new Error("withCredentials option was not a 'boolean' or 'undefined' value");n.withCredentials=void 0===n.withCredentials||n.withCredentials,n.timeout=void 0===n.timeout?1e5:n.timeout;let o=null,r=null;if(g.isNode){const t=require;o=t("ws"),r=t("eventsource")}g.isNode||"undefined"==typeof WebSocket||n.WebSocket?g.isNode&&!n.WebSocket&&o&&(n.WebSocket=o):n.WebSocket=WebSocket,g.isNode||"undefined"==typeof EventSource||n.EventSource?g.isNode&&!n.EventSource&&void 0!==r&&(n.EventSource=r):n.EventSource=EventSource,this.$=new F(n.httpClient||new D(this.u),n.accessTokenFactory),this.ut="Disconnected",this.dt=!1,this.ne=n,this.onreceive=null,this.onclose=null}async start(t){if(t=t||B.Binary,m.isIn(t,B,"transferFormat"),this.u.log(e.Debug,`Starting connection with transfer format '${B[t]}'.`),"Disconnected"!==this.ut)return Promise.reject(new Error("Cannot start an HttpConnection that is not in the 'Disconnected' state."));if(this.ut="Connecting",this.Ee=this.yt(t),await this.Ee,"Disconnecting"===this.ut){const t="Failed to start the HttpConnection before stop() was called.";return this.u.log(e.Error,t),await this.It,Promise.reject(new r(t))}if("Connected"!==this.ut){const t="HttpConnection.startInternal completed gracefully but didn't enter the connection into the connected state!";return this.u.log(e.Error,t),Promise.reject(new r(t))}this.dt=!0}send(t){return"Connected"!==this.ut?Promise.reject(new Error("Cannot send data if the connection is not in the 'Connected' State.")):(this.$e||(this.$e=new Y(this.transport)),this.$e.send(t))}async stop(t){return"Disconnected"===this.ut?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnected state.`),Promise.resolve()):"Disconnecting"===this.ut?(this.u.log(e.Debug,`Call to HttpConnection.stop(${t}) ignored because the connection is already in the disconnecting state.`),this.It):(this.ut="Disconnecting",this.It=new Promise((t=>{this.ye=t})),await this._t(t),void await this.It)}async _t(t){this.Ce=t;try{await this.Ee}catch(t){}if(this.transport){try{await this.transport.stop()}catch(t){this.u.log(e.Error,`HttpConnection.transport.stop() threw error '${t}'.`),this.Se()}this.transport=void 0}else this.u.log(e.Debug,"HttpConnection.transport is undefined in HttpConnection.stop() because start() failed.")}async yt(t){let n=this.baseUrl;this.Zt=this.ne.accessTokenFactory,this.$.Zt=this.Zt;try{if(this.ne.skipNegotiation){if(this.ne.transport!==U.WebSockets)throw new Error("Negotiation can only be skipped when using the WebSocket transport directly.");this.transport=this.ke(U.WebSockets),await this.Pe(n,t)}else{let e=null,i=0;do{if(e=await this.Te(n),"Disconnecting"===this.ut||"Disconnected"===this.ut)throw new r("The connection was stopped during negotiation.");if(e.error)throw new Error(e.error);if(e.ProtocolVersion)throw new Error("Detected a connection attempt to an ASP.NET SignalR Server. This client only supports connecting to an ASP.NET Core SignalR Server. See https://aka.ms/signalr-core-differences for details.");if(e.url&&(n=e.url),e.accessToken){const t=e.accessToken;this.Zt=()=>t,this.$.te=t,this.$.Zt=void 0}i++}while(e.url&&i<100);if(100===i&&e.url)throw new Error("Negotiate redirection limit exceeded.");await this.Ie(n,this.ne.transport,e,t)}this.transport instanceof X&&(this.features.inherentKeepAlive=!0),"Connecting"===this.ut&&(this.u.log(e.Debug,"The HttpConnection connected successfully."),this.ut="Connected")}catch(t){return this.u.log(e.Error,"Failed to start the connection: "+t),this.ut="Disconnected",this.transport=void 0,this.ye(),Promise.reject(t)}}async Te(t){const n={},[o,r]=C();n[o]=r;const s=this._e(t);this.u.log(e.Debug,`Sending negotiation request: ${s}.`);try{const t=await this.$.post(s,{content:"",headers:{...n,...this.ne.headers},timeout:this.ne.timeout,withCredentials:this.ne.withCredentials});if(200!==t.statusCode)return Promise.reject(new Error(`Unexpected status code returned from negotiate '${t.statusCode}'`));const e=JSON.parse(t.content);return(!e.negotiateVersion||e.negotiateVersion<1)&&(e.connectionToken=e.connectionId),e.useStatefulReconnect&&!0!==this.ne.He?Promise.reject(new l("Client didn't negotiate Stateful Reconnect but the server did.")):e}catch(t){let n="Failed to complete negotiation with the server: "+t;return t instanceof i&&404===t.statusCode&&(n+=" Either this is not a SignalR endpoint or there is a proxy blocking the connection."),this.u.log(e.Error,n),Promise.reject(new l(n))}}De(t,e){return e?t+(-1===t.indexOf("?")?"?":"&")+`id=${e}`:t}async Ie(t,n,i,o){let s=this.De(t,i.connectionToken);if(this.Re(n))return this.u.log(e.Debug,"Connection was provided an instance of ITransport, using that directly."),this.transport=n,await this.Pe(s,o),void(this.connectionId=i.connectionId);const a=[],l=i.availableTransports||[];let d=i;for(const i of l){const l=this.xe(i,n,o,!0===(null==d?void 0:d.useStatefulReconnect));if(l instanceof Error)a.push(`${i.transport} failed:`),a.push(l);else if(this.Re(l)){if(this.transport=l,!d){try{d=await this.Te(t)}catch(t){return Promise.reject(t)}s=this.De(t,d.connectionToken)}try{return await this.Pe(s,o),void(this.connectionId=d.connectionId)}catch(t){if(this.u.log(e.Error,`Failed to start the transport '${i.transport}': ${t}`),d=void 0,a.push(new c(`${i.transport} failed: ${t}`,U[i.transport])),"Connecting"!==this.ut){const t="Failed to select transport before stop() was called.";return this.u.log(e.Debug,t),Promise.reject(new r(t))}}}}return a.length>0?Promise.reject(new u(`Unable to connect to the server with any of the available transports. ${a.join(" ")}`,a)):Promise.reject(new Error("None of the transports supported by the client are supported by the server."))}ke(t){switch(t){case U.WebSockets:if(!this.ne.WebSocket)throw new Error("'WebSocket' is not supported in your environment.");return new J(this.$,this.Zt,this.u,this.ne.logMessageContent,this.ne.WebSocket,this.ne.headers||{});case U.ServerSentEvents:if(!this.ne.EventSource)throw new Error("'EventSource' is not supported in your environment.");return new V(this.$,this.$.te,this.u,this.ne);case U.LongPolling:return new X(this.$,this.u,this.ne);default:throw new Error(`Unknown transport: ${t}.`)}}Pe(t,e){return this.transport.onreceive=this.onreceive,this.features.reconnect?this.transport.onclose=async n=>{let i=!1;if(this.features.reconnect){try{this.features.disconnected(),await this.transport.connect(t,e),await this.features.resend()}catch{i=!0}i&&this.Se(n)}else this.Se(n)}:this.transport.onclose=t=>this.Se(t),this.transport.connect(t,e)}xe(t,n,i,o){const r=U[t.transport];if(null==r)return this.u.log(e.Debug,`Skipping transport '${t.transport}' because it is not supported by this client.`),new Error(`Skipping transport '${t.transport}' because it is not supported by this client.`);if(!function(t,e){return!t||!!(e&t)}(n,r))return this.u.log(e.Debug,`Skipping transport '${U[r]}' because it was disabled by the client.`),new a(`'${U[r]}' is disabled by the client.`,r);if(!(t.transferFormats.map((t=>B[t])).indexOf(i)>=0))return this.u.log(e.Debug,`Skipping transport '${U[r]}' because it does not support the requested transfer format '${B[i]}'.`),new Error(`'${U[r]}' does not support ${B[i]}.`);if(r===U.WebSockets&&!this.ne.WebSocket||r===U.ServerSentEvents&&!this.ne.EventSource)return this.u.log(e.Debug,`Skipping transport '${U[r]}' because it is not supported in your environment.'`),new s(`'${U[r]}' is not supported in your environment.`,r);this.u.log(e.Debug,`Selecting transport '${U[r]}'.`);try{return this.features.reconnect=r===U.WebSockets?o:void 0,this.ke(r)}catch(t){return t}}Re(t){return t&&"object"==typeof t&&"connect"in t}Se(t){if(this.u.log(e.Debug,`HttpConnection.stopConnection(${t}) called while in state ${this.ut}.`),this.transport=void 0,t=this.Ce||t,this.Ce=void 0,"Disconnected"!==this.ut){if("Connecting"===this.ut)throw this.u.log(e.Warning,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is still in the connecting state.`),new Error(`HttpConnection.stopConnection(${t}) was called while the connection is still in the connecting state.`);if("Disconnecting"===this.ut&&this.ye(),t?this.u.log(e.Error,`Connection disconnected with error '${t}'.`):this.u.log(e.Information,"Connection disconnected."),this.$e&&(this.$e.stop().catch((t=>{this.u.log(e.Error,`TransportSendQueue.stop() threw error '${t}'.`)})),this.$e=void 0),this.connectionId=void 0,this.ut="Disconnected",this.dt){this.dt=!1;try{this.onclose&&this.onclose(t)}catch(n){this.u.log(e.Error,`HttpConnection.onclose(${t}) threw error '${n}'.`)}}}else this.u.log(e.Debug,`Call to HttpConnection.stopConnection(${t}) was ignored because the connection is already in the disconnected state.`)}be(t){if(0===t.lastIndexOf("https://",0)||0===t.lastIndexOf("http://",0))return t;if(!g.isBrowser)throw new Error(`Cannot resolve '${t}'.`);const n=window.document.createElement("a");return n.href=t,this.u.log(e.Information,`Normalizing '${t}' to '${n.href}'.`),n.href}_e(t){const e=new URL(t);e.pathname.endsWith("/")?e.pathname+="negotiate":e.pathname+="/negotiate";const n=new URLSearchParams(e.searchParams);return n.has("negotiateVersion")||n.append("negotiateVersion",this.ve.toString()),n.has("useStatefulReconnect")?"true"===n.get("useStatefulReconnect")&&(this.ne.He=!0):!0===this.ne.He&&n.append("useStatefulReconnect","true"),e.search=n.toString(),e.toString()}}class Y{constructor(t){this.Ae=t,this.Ue=[],this.Le=!0,this.Ne=new Q,this.qe=new Q,this.Me=this.je()}send(t){return this.We(t),this.qe||(this.qe=new Q),this.qe.promise}stop(){return this.Le=!1,this.Ne.resolve(),this.Me}We(t){if(this.Ue.length&&typeof this.Ue[0]!=typeof t)throw new Error(`Expected data to be of type ${typeof this.Ue} but was of type ${typeof t}`);this.Ue.push(t),this.Ne.resolve()}async je(){for(;;){if(await this.Ne.promise,!this.Le){this.qe&&this.qe.reject("Connection stopped.");break}this.Ne=new Q;const t=this.qe;this.qe=void 0;const e="string"==typeof this.Ue[0]?this.Ue.join(""):Y.Oe(this.Ue);this.Ue.length=0;try{await this.Ae.send(e),t.resolve()}catch(e){t.reject(e)}}}static Oe(t){const e=t.map((t=>t.byteLength)).reduce(((t,e)=>t+e)),n=new Uint8Array(e);let i=0;for(const e of t)n.set(new Uint8Array(e),i),i+=e.byteLength;return n.buffer}}class Q{constructor(){this.promise=new Promise(((t,e)=>[this.j,this.Fe]=[t,e]))}resolve(){this.j()}reject(t){this.Fe(t)}}class K{constructor(){this.name="json",this.version=2,this.transferFormat=B.Text}parseMessages(t,n){if("string"!=typeof t)throw new Error("Invalid input for JSON hub protocol. Expected a string.");if(!t)return[];null===n&&(n=p.instance);const i=A.parse(t),o=[];for(const t of i){const i=JSON.parse(t);if("number"!=typeof i.type)throw new Error("Invalid payload.");switch(i.type){case N.Invocation:this.U(i);break;case N.StreamItem:this.Be(i);break;case N.Completion:this.Xe(i);break;case N.Ping:case N.Close:break;case N.Ack:this.Je(i);break;case N.Sequence:this.ze(i);break;default:n.log(e.Information,"Unknown message type '"+i.type+"' ignored.");continue}o.push(i)}return o}writeMessage(t){return A.write(JSON.stringify(t))}U(t){this.Ve(t.target,"Invalid payload for Invocation message."),void 0!==t.invocationId&&this.Ve(t.invocationId,"Invalid payload for Invocation message.")}Be(t){if(this.Ve(t.invocationId,"Invalid payload for StreamItem message."),void 0===t.item)throw new Error("Invalid payload for StreamItem message.")}Xe(t){if(t.result&&t.error)throw new Error("Invalid payload for Completion message.");!t.result&&t.error&&this.Ve(t.error,"Invalid payload for Completion message."),this.Ve(t.invocationId,"Invalid payload for Completion message.")}Je(t){if("number"!=typeof t.sequenceId)throw new Error("Invalid SequenceId for Ack message.")}ze(t){if("number"!=typeof t.sequenceId)throw new Error("Invalid SequenceId for Sequence message.")}Ve(t,e){if("string"!=typeof t||""===t)throw new Error(e)}}const G={trace:e.Trace,debug:e.Debug,info:e.Information,information:e.Information,warn:e.Warning,warning:e.Warning,error:e.Error,critical:e.Critical,none:e.None};class tt{configureLogging(t){if(m.isRequired(t,"logging"),void 0!==t.log)this.logger=t;else if("string"==typeof t){const e=function(t){const e=G[t.toLowerCase()];if(void 0!==e)return e;throw new Error(`Unknown log level: ${t}`)}(t);this.logger=new w(e)}else this.logger=new w(t);return this}withUrl(t,e){return m.isRequired(t,"url"),m.isNotEmpty(t,"url"),this.url=t,this.httpConnectionOptions="object"==typeof e?{...this.httpConnectionOptions,...e}:{...this.httpConnectionOptions,transport:e},this}withHubProtocol(t){return m.isRequired(t,"protocol"),this.protocol=t,this}withAutomaticReconnect(t){if(this.reconnectPolicy)throw new Error("A reconnectPolicy has already been set.");return t?Array.isArray(t)?this.reconnectPolicy=new z(t):this.reconnectPolicy=t:this.reconnectPolicy=new z,this}withServerTimeout(t){return m.isRequired(t,"milliseconds"),this.Ke=t,this}withKeepAliveInterval(t){return m.isRequired(t,"milliseconds"),this.Ge=t,this}withStatefulReconnect(t){return void 0===this.httpConnectionOptions&&(this.httpConnectionOptions={}),this.httpConnectionOptions.He=!0,this.Y=null==t?void 0:t.bufferSize,this}build(){const t=this.httpConnectionOptions||{};if(void 0===t.logger&&(t.logger=this.logger),!this.url)throw new Error("The 'HubConnectionBuilder.withUrl' method must be called before building the connection.");const e=new Z(this.url,t);return O.create(e,this.logger||p.instance,this.protocol||new K,this.reconnectPolicy,this.Ke,this.Ge,this.Y)}}return Uint8Array.prototype.indexOf||Object.defineProperty(Uint8Array.prototype,"indexOf",{value:Array.prototype.indexOf,writable:!0}),Uint8Array.prototype.slice||Object.defineProperty(Uint8Array.prototype,"slice",{value:function(t,e){return new Uint8Array(Array.prototype.slice.call(this,t,e))},writable:!0}),Uint8Array.prototype.forEach||Object.defineProperty(Uint8Array.prototype,"forEach",{value:Array.prototype.forEach,writable:!0}),n})(),"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.signalR=e():t.signalR=e(),$.extend($t,{t1:"Eingabe erforderlich",t2:"Bitte überprüfen Sie Ihre Eingaben im Formular.",b0:"Erstellt",b1:"Zuletzt geändert",b2:"von",t12:"Der Server hat einen Fehler zurückgegeben. Bitte versuchen Sie es erneut.",t17:"Eine Email mit einem Aktivierungs-Link wurde an deine Adresse versandt.",t18:"Ein Account mit deinem Namen existiert bereits. Dennoch erstellen?",t19:"Einträge sind entweder unngültig oder zu kurz.",t20:"Der Server hat einen Fehler gemeldet. Bitte versuch es erneut.",t21:"Der Zugang wurde nicht gefunden.",t30a:"Als erledigt markieren.",t30b:"Als unerledigt markieren.",t55:"Ein Email mit einem Aktivierungs-Link wurde an Ihre Adresse versandt.",t56:"Ein Zugang für diesen Namen besteht bereits. Trotzdem erstellen?",t57:"Ein bestehender Zugang wurde für diese Serie registriert.",t60:"Bitte geben Sie Email-Adresse an, die Sie hier hinterlegt haben.",t61:"Ihr Passwort wurde erfolgreich versandt.",t62:"Die angegebene Email-Adresse stimmt nicht mit der hier hinterlegten überein.",ov:"Persönliche Übersicht"});var $v={}; /*! loadCSS. [c]2020 Filament Group, Inc. MIT License */ /*! onloadCSS. (onload callback for loadCSS) [c]2017 Filament Group, Inc. MIT License */ -function onloadCSS(t,e){e=e||{};let n=function(e){return new Promise(((n,o)=>{t.addEventListener?e.addEventListener("load",newcb):t.attachEvent&&e.attachEvent("onload",newcb),"isApplicationInstalled"in navigator&&"onloadcssdefined"in t&&e.onloadcssdefined(newcb)}))};if(Array.isArray(t)){let o=t.length;Promise.all(t.map(n)).then((function(t){var n=t.reduce(((t,e)=>t+(!0===e?1:0)));!async function(t){!0===t&&"function"==typeof e.success?e.success():!0===t&&"object"==typeof e.success&&e.success instanceof Promise&&await e.success(),e.complete()}(o===n)}))}else n(t)}!function(t){"use strict";var e=function(e,n,o,i){var r,s=t.document,a=s.createElement("link");if(n)r=n;else{var c=(s.body||s.getElementsByTagName("head")[0]).childNodes;r=c[c.length-1]}var l=s.styleSheets;if(i)for(var u in i)i.hasOwnProperty(u)&&a.setAttribute(u,i[u]);a.rel="stylesheet",a.href=e,a.media="only x",function t(e){if(s.body)return e();setTimeout((function(){t(e)}))}((function(){r.parentNode.insertBefore(a,n?r:r.nextSibling)}));var d=function(t){for(var e=a.href,n=l.length;n--;)if(l[n].href===e)return t();setTimeout((function(){d(t)}))};function h(){a.addEventListener&&a.removeEventListener("load",h),a.media=o||"all"}return a.addEventListener&&a.addEventListener("load",h),a.onloadcssdefined=d,d(h),a};"undefined"!=typeof exports?exports.loadCSS=e:t.loadCSS=e}("undefined"!=typeof global?global:this);const isIE=/MSIE\/|Trident/gi.test(window.navigator.userAgent)||void 0!==window.document.documentMode,isfileapi=!!(window.File&&window.FileReader&&window.FileList&&window.Blob);var $ocms={auth:{},no:function(t){t.stopPropagation()},vmin:function(t){var e=$(window).width*(t||1),n=$(window).height*(t||1);return e($ocms.baseurl+"/"+(t||"")).replace(/\/\//,"/"),cexi:null};function deepCopy(t){var e,n,o;if("object"!=typeof t||null===t)return t;for(o in e=Array.isArray(t)?[]:{},t)n=t[o],e[o]=deepCopy(n);return e}function fields_definition(t,e,n){this.label_sng=!0===Array.isArray(t)?"":t||"",this.label_pl=!0===Array.isArray(t)?"":e||"",this.fields=!0===Array.isArray(t)?t:n||[],this.itm=function(t){for(var e=0;e0)for(var n=0;nt||"")).filter(((t,e)=>""!==t)).join(e)}function parseDt(t,e,n){t=(t||"").substr(0,e.length);var o=e,i=t.length>0&&e.split(";").some((function(e){for(var n,i=/[^yMdhms0-9]/gi,r=!0;null!==(n=i.exec(e));)r=r&&e.substr(n.index,1)===t.substr(n.index,1);var s=t.length===e.length&&r;return!0===s&&(o=e),s}));if(!0===i){for(var r,s=[0,0,0,0,0,0,0],a=/(mm{1,2}|dd{1,2}|MM{1,2}|(yy){2,4}|ss{1,2}|hh{1,2})(?!\w)/g;null!==(r=a.exec(o));)s["yMdhms".indexOf(r[0].substr(0,1))]=parseInt(("yy"===r[0]?"20":"")+t.substr(r.index,r[0].length))-("M"===r[0].substr(0,1)?1:0);var c=new(Function.prototype.bind.apply(Date,[null].concat(s)));return"string"==typeof n?fdt(c,n):c}return!1}function bool(t,e){return"boolean"==typeof t?t:"boolean"==typeof e&&e}function booln(t,e){return"boolean"==typeof t?t:"number"==typeof t?1===t:"boolean"==typeof e&&e}Date.prototype.isValid=function(){return!isNaN(this)},Date.prototype.format=function(t){return fdt(this,t)},Date.prototype.addDays=function(t){return this.setDate(this.getDate()+t),this},Date.prototype.isBetween=function(t,e){return this>t&&this section");$(window).scroll((function(e){let n=$(window).scrollTop(),o=$("body");o.toggleClass("unfocus",n>vh()-1.2*t),o.toggleClass("btb",n>.5*vh()-t)}))},$ocms.cf_reset=function(){return $("#contentframe").empty()},function(t){t.fn.scrollTo=function(e){if(t(this).length>0){var n=t(this).offset().top||0;n>0&&t("html, body").animate({scrollTop:n-hh()},2e3)}},t.fn.ldng=function(e){var n=!0;return"boolean"==typeof e?n=e:"number"==typeof e&&(n=e>0),t(this).toggleClass("loading",n)},"function"!=typeof t.noop&&(t.noop=function(){}),t.fn.hasAttr=function(e){var n=t(this).attr(e);return void 0!==n&&!1!==n},t.fn.parseCssPx=function(e){try{return parseFloat(t(this).css(e).replace("px","")||0)}catch(t){return 0}},t.max=function(t,e){return isNaN(t)&&isNaN(e)?null:isNaN(t)&&!isNaN(e)?e:!isNaN(e)&&isNaN(e)||t>=e?t:e},t.min=function(t,e){return isNaN(t)&&isNaN(e)?null:isNaN(t)&&!isNaN(e)?e:!isNaN(e)&&isNaN(e)||t<=e?t:e},t.lim=function(t,e){return isNaN(t)?null:isNaN(e)?t:e<=t?e:t},t.fn.enterKey=function(e){return this.each((function(){t(this).keypress((function(t){"13"===(t.keyCode?t.keyCode:t.which).toString()&&e.call(this,t)}))}))}}(jQuery),$ocms.defaultTimeout=3e4,$ocms.AjaxEX=function(t){var e=this;e.responseText=e.responseText||"";var n=e.getResponseHeader("x-ocms-code")||"";e.internalCode=""!==n&&!1===isNaN(n)?parseInt(n):-1,e.isInternal=e.internalCode>-1,e.internalText=decodeURIComponent((e.getResponseHeader("x-ocms-desc")||"").replace(/\+/g,"%20")||"");var o=e.internalText||t,i=e.internalCode||e.status;e.logtext=o+" ("+i+")"},$ocms.postXTS=function(t){$ocms.postXT.call(this,$.extend(t,{sync:!0}))},$ocms.postXT=function(t){if((t=t||{}).trycount=t.trycount||0,""!==(t.url||"")){t.url=-1!==t.url.indexOf("&yy=")?t.url:t.url.indexOf("?")>-1?t.url+"&yy="+(new Date).getTime():t.url+"?yy="+(new Date).getTime();var e=t.context||this;switch(t.context=e,t.retryLimit=t.retryLimit||0,t.timeout=t.timeout||$ocms.defaultTimeout,t.timeout<100&&(t.timeout=1e3*t.timeout),t.data=t.data||{},t.contentType=t.contentType||"multipart/form-data; charset=UTF-8",t.islogin="boolean"==typeof t.islogin&&t.islogin,t.contentType){case"":case"json":t.contentType="application/json; charset=utf-8";break;case"form":t.contentType="application/x-www-form-urlencoded; charset=UTF-8";break;case"multi":t.contentType="multipart/form-data";break;case"text":t.contentType="text/plain; charset=UTF-8"}if(t.form instanceof jQuery?(t.data=t.form.serializeObject(),t.contentType="form-data"):t.lzw instanceof jQuery&&(t.data.lzw=$.ccLZW(t.lzw.serializeAnything(!0)).join(",")),"multipart/form-data"!==t.contentType.substr(0,19)&&"form-data"!==t.contentType.substr(0,9)||t.data instanceof FormData!=!1)t.data instanceof FormData&&(t.contentType=!1,t.processData=!1);else{t.contentType=!1;var n=new FormData;$.each(t.files||[],(function(t,e){n.append("upload_file",e)})),$.each(t.data||{},(function(t,e){n.append(t,e)})),t.data=n,t.processData=!1}var o={type:t.method||"post",url:t.url,data:t.data,processData:"boolean"!=typeof t.processData||t.processData,contentType:t.contentType,cache:t.cache||!1,timeout:t.timeout,beforeSend:function(n){$(t.loading).ldng(),$("body").addClass("ldng"),"function"==typeof t.beforesend&&t.beforesend.apply(e,[n])},success:function(n,o,i){"false"===n||"not authorized"===n?("function"==typeof t.error&&t.error.apply(e,[i,o,n]),"function"==typeof $.status&&$.status(o+" - "+n)):"function"==typeof t.success&&t.success.apply(e,[n,o,i])},error:function(n,o,i){if($ocms.AjaxEX.call(n,o),-1===t.url.indexOf("doc.ashx")||-1!==t.url.indexOf("ftest")){if(401===n.status&&111===n.internalCode&&!1===t.islogin&&"function"==typeof $ocms.login.dlg)$ocms.login.dlg({ajo:t});else if("timeout"===o||302===n.status)return t.tryCount++,t.tryCount<=t.retryLimit?void $ocms.postXT(t):void 0;"function"==typeof t.error?t.error.apply(e,[n,o,i]):"function"==typeof $ocms.failure?$ocms.failure.apply(e,[n]):"function"==typeof $.status&&$.status("Server error: "+o+" - "+i)}},dataType:t.datatype||"json",complete:function(n,o){"function"==typeof t.complete&&t.complete.apply(e,[n,o]),$(t.loading).ldng(0),$("body").removeClass("ldng");let i=$("body > .timer");if(i.length>0){let t=new Date(n.getResponseHeader("ocms_cec")||""),e=new Date(n.getResponseHeader("ocms_cex")||"");if(t.isValid()&&e.isValid()){let n=new Date,o=Math.abs(e-t);n.setMilliseconds(n.getMilliseconds()+o),i.data({cex:n,ctt:o}),$ocms.cex_timer()}}},context:e,async:!0};"boolean"==typeof t.sync&&(o.async=!1===t.sync),!0==("boolean"==typeof t.contentType&&!1===t.contentType)&&(o.contentType=!1),$.ajax(o)}},$ocms.cex_timer=function(){$ocms.cexi||($ocms.cexi=setInterval($ocms.cex_timer,15e3));let t=$("body > .timer"),e=t.data("cex"),n=t.data("ctt"),o=new Date;if(e instanceof Date&&e.isValid()&&"number"==typeof n&&n>0&&e>o){let i=Math.abs(o-e)/n*100;t.css("width",i.toString()+"%"),i<98&&(!$ocms.cex_lp||Math.abs(o-$ocms.cex_lp)>6e5)&&$ocms.postXT({url:$ocms.url("ping"),success:()=>{$ocms.cex_lp=o},timeout:5e3,error:()=>{}})}},$ocms.vbl_send=function(t){var e=t.data||{};if(""!==(e.url||"")){var n=$("#contentframe form:first"),o={url:e.url,data:new FormData,success:function(t){"function"==typeof e.success?e.success(t):"string"==typeof e.success&&alert(e.success)},error:function(t,n,o){"function"==typeof e.error?e.error(o):"string"==typeof e.error&&alert(e.error)},complete:function(){n.ldng(0)}},i=!0;n.find("input").each((function(){var t=$(this),e=t.nza("name"),n=t.val(),r=$(this).prop("required")||!1;if(""!==e){var s=""!==n||!1===r;i=i&&s,!0===s?(o.data.append(e,n),t[0].setCustomValidity("")):""!==$(this).nza("ocms-nvnote")&&t[0].setCustomValidity($(this).nza("ocms-nvnote"))}})),!0===i&&(n.ldng(1),$ocms.postXT.call(this,o))}},function(t){t.fn.nza=function(e,n){var o=t(this).attr(e);return void 0!==o&&!1!==o?o:n||""},t.fn.serializeObject=function(e,n){var o=/\r?\n/g,i=/^(?:submit|button|image|reset|file)$/i,r=/^(?:input|select|textarea|keygen)/i,s=/^(?:checkbox|radio)$/i,a=bool((n=n||{}).typedvalues,!1),c={},l=t(this),u=l.find(':input:not([nosend],[type="file"])').addBack(":input"),d=!0;return t.each(u.not(".tinymce").get(),(function(n,l){var u=t(this),h=this,p=(this.type||"").toLowerCase(),f=u.prop("required")||!1;if(!0===(h.name&&!u.is(":disabled")&&r.test(h.nodeName)&&!i.test(p))){var m=u.val(),g=h.name,y=u.nza("data-format").split(":"),b=u.nza("pattern")||".*";if(!0===s.test(p)&&(m=h.checked?""!==m?m:"true":""),"date"===y[0].substr(0,4)&&y.length>1)"boolean"==typeof(m=parseDt(m,y.slice(1).join(":")))&&(m=null),null===m&&"date"===u.prop("type").substr(0,4)&&!1===isNaN(new Date(u.val()))&&(m=new Date(u.val())),m instanceof Date==!0&&"function"==typeof m.getMonth?!1===a&&(m=fdt(m,"date"===y[0]?"dts":"iso")):m=null;else if("number"===p&&!0===a){let t;t="integer"===y[0]?parseInt(m):parseFloat(m),m=isNaN(t)?m:t}if(!0!==f||""!==(m||"")&&null!==m.match(b)?!0===bool(e,!1)&&h.setCustomValidity(""):(!0===bool(e,!1)&&h.setCustomValidity(u.nza("ocms-nvnote",$ocms.t.inv||"Invalid field")),m=null),null!=m&&"string"==typeof m){let t=c[g];null!=t?Array.isArray(t)?t.push(m.replace(o,"\r\n")):c[g]=[t,m.replace(o,"\r\n")]:c[g]=m.replace(o,"\r\n")}else if(null!=m){let t=c[g];null!=t?Array.isArray(t)?t.push(m):c[g]=[t,m]:c[g]=m}else d=!1}})),u.filter(".tinymce").each((function(e,n){var o=t(this),i=((this.type||"").toLowerCase(),o.prop("required")||!1);try{var r=tinymce.get(t(n).attr("id"));if(r){var s=t(n).attr("name"),a=r.getContent();!1===i||""!==(a||"")?c[s]=a:d=!1}}catch(e){t.noop()}})),l.toggleClass("invalid",!d),d?c:null},t.fn.sendForm=function(e,n,o){var i=t(this);o=o||{};var r={url:e,success:function(t){if(o.response=t,"function"==typeof n)n(t);i.closest("div.modal").remove()},error:function(t,e,n){"function"==typeof o.error?o.error.call(this,t):$ocms.failure.call(this,t)},complete:function(){i.ldng(0),"function"==typeof o.complete&&o.complete.call(this,jqXHR)}},s=i.find('input[type="file"]');r.data=new FormData,s.length>0&&t.each(s[0].files,(function(t,e){r.data.append(t,e),r.data.append("file_lastmodified",$ocms.isodt(e.lastModifiedDate))}));var a=i.serializeObject();t.each(a||{},(function(t,e){r.data.append(t,e)})),i.ldng(),$ocms.postXT.call(this,r)},t.fn.checkValidity=function(){var e=t(this),n=!0;return e.each((function(t,e){n=n&&e.checkValidity()})),n},t.fn.wrap=function(e,n){var o=t(this),i=$$.dc(e).attr(n||{}).insertAfter(o);return o.append(i),i}}(jQuery),$ocms.logout=function(){$ocms.postXT({url:$ocms.url("logout"),complete:function(){window.location.reload()}})},$ocms.login={send:function(t){t.preventDefault();var e=$(this);if(!0===e.find("#dbtn-confirm").hasClass("disabled"))return!1;var n=e.serializeObject();return n.loginaccount=ne(n.loginaccount,$ocms.auth.account||$ocms.auth.requestedaccount||""),n.loginaccount=ne(n.loginaccount,$ocms.auth.account||$ocms.auth.requestedaccount||""),""===ne(n.loginaccount)&&!0===bool($ocms.auth.accountrequired,!0)?(alert($t.l16),!1):($ocms.postXT({url:$ocms.url("login"),data:n,success:function(){window.location.reload()}}),!1)},uichange:function(){let t=$(this),e=t.closest("form"),n=bool($ocms.auth.accountrequired,!0),o=ne(e.find('[name="loginaccount"]').val(),$ocms.auth.account||$ocms.auth.requestedaccount||"");if(""!==o||!1===n){var i=e.find('[name="userlogin"]').empty().val(""),r=e.find('[name="username"]').empty().val(""),s=$("#dlg_userlogin_sel").empty().val(""),a=t.val()||"";if(!1===t.checkValidity()&&""===a)return;var c=t.closest("table").ldng();$ocms.postXT.call(this,{url:$ocms.url("auth"),data:{userinfo:a,account:o||""},success:function(t,e,n){if(1===t.length){var o=t[0];i.val(o.login).change().attr("required","").removeAttr("nosend"),r.val(o.name).change().attr("required","").show(),s.removeAttr("required").attr("nosend","").hide()}else t.length>0?(r.hide().removeAttr("required"),i.removeAttr("required").attr("nosend",""),0===s.length&&(s=$("").attr({name:"userlogin",size:t.length,id:"dlg_userlogin_sel",class:"form-control",required:""}).css({width:"100%","max-width":"100%",padding:"2px"}).insertAfter(r)),$.each(t,(function(t,e){var n=$("").attr({value:e.login,style:"padding-top: 2px; padding-bottom: 5px;","border-bottom":"1px solid #EEE;"}).text(e.name).appendTo(s);t%2==0&&n.css({"background-color":"#F9F9F9"})})),s.attr("required","").removeAttr("nosend")):(s.hide().attr("nosend",""),r.attr("required","").show(),i.attr("required","").removeAttr("nosend"),alert($t.l9))},error:function(t){$ocms.failure.call(this,t)},complete:function(){c.ldng(0)}})}else alert($t.l18)},sendpassword:function(t){var e=$(''),n=e.find(".form-body"),o=null;e.find("form").submit((function(t){t.preventDefault();var i=$(this).serializeObject(!0),r=null===o,s=r?"spwc":"spw";return $ocms.postXT.call(this,{url:$ocms.url(s),data:i,complete:function(){r?(n.append('
Ihnen wurde ein Code per SMS zugesandt.
Bitte tragen Sie den hier ein:
'),o=$('
').appendTo(n)):(alert($t.l13),e.remove())},error:()=>{}}),!1})),e.find(".modal-close").click((function(){e.remove()}));var i=[];$.each($t.l7a.split("\n"),((t,e)=>{Array.prototype.push.apply(i,[$("
"),$("").text(e)])})),e.find(".modal-note").append($('').text($t.alert)).append(i),e.appendTo("body"),setTimeout((function(){$(".modal").find('input[name="lastname"]').focus()}),600)}};var $$={s:function(t){return $("").text(t)},br:function(){return $("
")},sc:function(t,e){return $("").addClass(t).text(e)},td:function(t,e){var n=$("");return t instanceof jQuery==!0?n.appendTo(t):"object"==typeof t?n.attr(t):"string"==typeof t&&n.text(t),"object"==typeof e?n.attr(e):"string"==typeof e&&n.text(e),n},th:function(t,e){var n=$("");return t instanceof jQuery==!0?n.appendTo(t):"object"==typeof t?n.attr(t):"string"==typeof t&&n.text(t),"object"==typeof e?n.attr(e):"string"==typeof e&&n.text(e),n},tdc:function(t,e,n){return $$.td(e,n).addClass(t)},td2:function(t){var e=$('');return"string"===$.type(t)?e.text(t):t instanceof jQuery?e.append(t):"function"==typeof t?t.call(e):e.html(" "),e},td3:function(t){var e=$('');return"string"===$.type(t)?e.text(t):t instanceof jQuery?e.append(t):"function"==typeof t?t.call(e):e.html(" "),e},tdtr:function(t,e){var n=$$.tr().appendTo(e);return t instanceof jQuery==!0||"string"==typeof t?t.appendTo($$.td().appendTo(n)):!0===Array.isArray(t)&&$.each(t,(function(t,e){$(e).appendTo($$.td().appendTo(n))})),n},tr:function(t,e){var n=$("");return t instanceof jQuery==!0?n.appendTo(t):"object"==typeof t&&n.attr(t),"object"==typeof e&&n.attr(e),n},trc:function(t,e){var n=$("").addClass(t);return e instanceof jQuery==!0?n.appendTo(e):"object"==typeof e&&n.attr(e),n},d:function(t){return $("
").attr(t||{})},dc:function(t,e,n,o){var i=$("
").addClass(t);return e instanceof jQuery==!0?i.appendTo(e):"object"==typeof e?i.attr(e):"function"==typeof e?i.click(e):"string"==typeof e&&i.text(e),"string"==typeof n?i.text(n):"object"==typeof n?i.attr(n):"function"==typeof n&&i.click(n),"string"==typeof o?i.text(o):"object"==typeof o?i.attr(o):"function"==typeof o&&i.click(o),i},df:function(t){return $("
 
").attr(t||{})},opt:function(t,e,n){var o=$("");return"string"==typeof t?o.attr("value",t):"object"==typeof t&&o.attr(t),"string"==typeof e?o.text(e):"object"==typeof e&&o.attr(e),"object"==typeof n&&o.attr(n),o},eOpt:function(t){var e=$('');return t&&e.attr("selected","selected"),e},tbl:function(t){return $("
").attr(t||{})},tblc:function(t){return $("
").addClass(t)},thead:function(t){let e=$("");return t instanceof jQuery&&e.prependTo(t),e},tbody:function(t){let e=$("");return t instanceof jQuery&&e.appendTo(t),e},tblset:function(t,e){let n=$$.tbl(t||{});return e instanceof jQuery&&e.append(n),{tbl:n,hd:$$.thead().appendTo(n),bdy:$$.tbody().appendTo(n)}},i:function(t){return $("").attr(t||{})},img:function(t,e){return $("").attr("src",t).attr(e||{})},sel:function(t){return $("").attr(t||{})},btn:function(t){return $("").attr(t||{})},a:function(t){return $("").attr(t||{})},li:function(t){return $("
  • ").attr(t||{})},ul:function(t){return $("
      ").attr(t||{})},nav:function(t){return $("").attr(t||{})},lbl:function(t,e){var n=$("");return"string"==typeof t&&n.text(t),"object"==typeof t?n.attr(t):"object"==typeof e&&n.attr(e),n},txt:function(t){return $("").attr(t||{})},0:function(t,e){return $("<"+t+">").attr(e||{})},bbtn:function(t,e){return $$.btn({type:"button",class:"btn"}).addClass(e).text(t)},svg:t=>$(document.createElementNS("http://www.w3.org/2000/svg",t))};function getMonday(t){var e=(t=new Date(t)).getDay(),n=t.getDate()-e+(0==e?-6:1);return new Date(t.setDate(n))}function $lf(t){var e=void 0===t?null:"number"==typeof t&&1!==t||"boolean"==typeof cl&&!1===t;return $("#listframe").tC("hd",e).is(".hd")}function $nuf(t){if(t&&t.stopPropagation(),!$(this).is(".disabled")){var e=function(t){t.removeClass("vis").find("li.dropdown").removeClass("open").removeClass("vis").attr("aria-expanded","false")},n=$(this).parent("li.dropdown");if(n.length>0){n.tC("open"),navs=!0===n.is(".open")?"true":"false",n.attr("aria-expanded",navs);var o=n.closest("nav");o.find("li.dropdown").not(n.parentsUntil("nav")).not(n).removeClass("open").attr("aria-expanded","false"),!1===n.is(".open")&&n.find("li.dropdown").removeClass("open").attr("aria-expanded","false"),e($("nav").not(o))}else e($("nav"))}}function $tbr(){return $lf(0),$("#topbar").ocmsmenu([])}function $lfr(){return $("#sidebar").empty(),$("#listframe").removeClass("fix").addClass("hd").empty()}function $cfr(){return $tbr(),$("#contentframe").empty()}function jObj(t,e){let n={};if("{"===(t||"").substr(0,1))try{n=JSON.parse(t)}catch(t){n={}}return n[e]||""}function string(t,e){var n,o=t||"";return $.each(e||[],(function(t,e){n=new RegExp("\\{"+t.toString()+"\\}","ig"),o=o.replace(n,e)})),o}function init_tooltip(t){var e=!0===("boolean"==typeof t&&t)&&"mouse";$("[title]").qtip({position:{target:e,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},effect:!1}),$("div.tooltiptext").each((function(){$(this).filter(":not(:empty)").parent().qtip({suppress:!1,content:{text:$(this).clone()},position:{target:e,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden}})}))}String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")},String.prototype.left=function(t){if("string"===$.type(t)){var e=this.indexOf(t);return e>0?this.slice(0,e):""}return this.substring(0,t)},String.prototype.right=function(t){if("string"===$.type(t)){var e=this.indexOf(t);return e>0?this.substring(this.length-e):""}return this.substring(this.length-t)},Array.prototype.move=function(t,e){if(e>=this.length)for(var n=e-this.length;1+n--;)this.push(void 0);return this.splice(e,0,this.splice(t,1)[0]),this},function(t){t.fn.appendToIf=function(e,n){var o=t(this),i="function"==typeof n?n(o):n;return!0===("boolean"!=typeof i||i)&&o.appendTo(e),o},t.fn.appendIf=function(e,n){var o=t(this),i="function"==typeof n?n(o):n;return!0===("boolean"!=typeof i||i)&&o.append(e),o},t.fn.rwText=function(e,n,o){var i=t(this).empty();o=t.extend({wrap:!0},o);var r=!0===Array.isArray(e)?e:(null==e?"":String(e)).split("\n");return t.each(r,(function(t,e){""!==(e||"")&&(t>0&&i.append($$.br()),i.append(!0===o.wrap?$$.s(e):e))})),n&&i.attr("title",n),i},t.fn.loadSel=function(e,n,o){if("SELECT"===t(this).prop("tagName").toUpperCase()){var i=t(this);$ocms.postXT.call(this,{url:e,data:n||{},success:function(e){t.each(e,(function(){i.append($$.opt(e.value,e.text))}))},complete:function(){i.ldng(0),"function"==typeof o&&o.call(i)}})}},t.fn.emptyWithEditors=function(e){var n=t(this);return n.find(":input.tinymce").each((function(e,n){try{var o=tinymce.get(t(n).attr("id"));o&&o.remove()}catch(e){t.noop()}})),n.empty()},t.fn.cssValue=function(t){if(this.length>0){var e=this.css(t)||"";if(""===e)return 0;var n=/(^[\d\.]*)(\D{1,3}$)/gi.exec(e);return null!==n?"rem"===n[2]?$ocms.rpx(parseFloat(n[1])):parseFloat(n[1]):!1===isNaN(e)?parseFloat(e):0}return 0},t.fn.veryInnerHeight=function(){let e=e=>t(this).cssValue(e);return t(this).innerHeight()-e("padding-top")-e("padding-bottom")},t.fn.veryInnerWidth=function(){let e=e=>t(this).cssValue(e);return t(this).innerWidth()-e("padding-left")-e("padding-right")},t.fn.marginWidth=function(){let e=e=>t(this).cssValue(e);return e("margin-left")+e("margin-right")},t.fn.marginHeight=function(){let e=e=>t(this).cssValue(e);return e("margin-top")+e("margin-bottom")},t.inArrayRegEx=function(e,n,o){var i="regexp"===t.type(e)?e:new RegExp(e);if(!n)return-1;for(var r=o=o||0;r7){o=e.split(","),i=(n||(t<0?"rgb(0,0,0)":"rgb(255,255,255)")).split(",");var c=a(o[0].slice(4)),l=a(o[1]),u=a(o[2]);return"rgb("+(s((a(i[0].slice(4))-c)*r)+c)+","+(s((a(i[1])-l)*r)+l)+","+(s((a(i[2])-u)*r)+u)+")"}var d=(o=a(e.slice(1),16))>>16,h=o>>8&255,p=255&o;return"#"+(16777216+65536*(s((((i=a((n||(t<0?"#000000":"#FFFFFF")).slice(1),16))>>16)-d)*r)+d)+256*(s(((i>>8&255)-h)*r)+h)+(s(((255&i)-p)*r)+p)).toString(16).slice(1)},t.fn.IN=function(e){return t(this).fadeIn(400,e),t(this)},t.fn.OUT=function(e){return t(this).fadeOut(400,e),t(this)},t.fn.tooltip=function(e,n){var o=!0===("boolean"==typeof e&&e)&&"mouse",i="boolean"==typeof n&&n,r=t(this);return r.each((function(){var e=i?t(this).find(".tooltiptext"):t(this).children(".tooltiptext");t(e).length>0?e.each((function(){var e=t(this);t(this).filter(":not(:empty)").parent().qtip({suppress:!1,content:{text:e.clone()},position:{target:o,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},show:{effect:!1},hide:{effect:!1}}),e.remove()})):t(this).qtip({position:{target:o,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},effect:!1})})),r},t.fn.rC=function(e){return t(this).removeClass(e)},t.fn.aC=function(e){return t(this).addClass(e)},t.fn.tC=function(e,n){return t(this).toggleClass(e,n)}}(jQuery),function(t){t.fn.ocmsmenu=function(e,n){var o=t(this);return $ocms.menu.call(o,e,n),o},t.fn.activatemenu=function(){var e=t(this).filter("nav");return e.find("a").not(".on").addClass("on").click($nuf),e.find(".nav-btn").not(".on").addClass("on").click((function(e){e.stopPropagation();var n=t(this);t(n.attr("data-target")).tC(n.attr("data-toggle"))})),e}}(jQuery);class ObjectArray extends Array{isEmpty(){return 0===this[0].length}static get[Symbol.species](){return Array}filter(t){return"function"==typeof t?new ObjectArray(this[0].filter(t)):this}remove(t){if("function"!=typeof t)return this;{let e=this[0].findIndex(t);for(;e>-1;)this[0].splice(e),e=this[0].findIndex(t)}}sortBy(t){return"function"==typeof t&&this[0].sort(t),this}sortString(t){return this[0].sort(((e,n)=>{let o=(e[t]||"").toString().toUpperCase(),i=(n[t]||"").toString().toUpperCase();return console.debug(o.localeCompare(i)),o.localeCompare(i)})),this}sortNum(t){return this[0].sort(((e,n)=>{let o=e[t],i=n[t];return!0===isNaN(i)&&!1===isNaN(o)||oi?1:0})),this}sum(t){return this[0].reduce(((e,n)=>e+(!0===isNaN(n[t])?0:n[t])),0)}groupBy(t){return this[0].reduce((function(e,n){let o=n[t];return e[o]||(e[o]=[]),e[o].push(n),e}),{})}each(t){if("function"==typeof t){let e=!1;this[0].forEach(((n,o,i)=>{if(!1===e){let r=t(n,o,i);"boolean"==typeof r&&!1===r&&(e=!0)}}))}}get toArray(){return this[0]}}class NumArray extends Array{sum(){return this.reduce(((t,e)=>t+e))}first(){return this[0]}last(){return this[this.length-1]}average(){return this.sum()/this.length}range(){let t=this.map((t=>t)).sort();return{min:t[0],max:t[this.length-1]}}static get[Symbol.species](){return Array}}$ocms.ocmsmenu=[{lbl:"",id:"m_home",ico:"glyphicon glyphicon-home",fnc:"init:home"},{fnc:"separator"}],function(t){t.multline=function(t){let e=t.split("\n"),n=$$.d();return $.each(e,((t,e)=>{n.append($$.s(e))})),n.html()},t.tooltip_hidden=function(t,e){$(this).remove(),e.rendered=!1},t.isJSONDateString=function(t){return"string"==typeof t&&/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/.test(t)},t.failure=function(e){11110===(e.internalCode||-1)?t.login.dlg():alert($t.f1+"\n"+(e.internalText||""))},t.getScript=function(e,n){var o=[],i=[],r=function(t){return"string"==typeof t&&""!==(t||"")},s=function(t,e){!0===bool(e.condition,!0)&&(""!==(e.script||"")&&i.push({url:e.script,module:e.module||""}),!0===r(e.css||"")?o.push(e.css):!0===Array.isArray(e.css)&&Array.prototype.push.apply(o,e.css.filter(r)))};!0===r(e||"")?i.push(e):!0===Array.isArray(e)?$.each(e,s):"object"==typeof e&&""!==(e.script||"")&&s(0,e);let a=[];$.each(o,(function(t,e){""!==(e||"")&&a.push(loadCSS(e))}));let c=i.map((function(e,n){let o=e.url,r=e.module||"";if(""===r){let t=new Promise((function(t,e){try{!async function(){$.ajax({url:o,dataType:"script",success:function(){t(i)},error:function(){e(i)},timeout:3e4})}()}catch(t){console.debug(t.message+"%o",t)}}));return t}return t.loadmodule(r,o,e.alias)}));Promise.all(c).then(n)},t.loadmodule=function(e,n,o){let i=new Promise((function(i,r){!async function(){try{let s=(n.startsWith("/")||n.startsWith(".")?"":"/")+n;import(s).then((n=>{t[e]=n[o||"default"],i(e)})).catch((t=>{console.debug(t.message+"%o",t),r(e)}))}catch(t){console.debug(t.message+"%o",t)}}()}));return i},t.ocms_auth=function(e,n,o,i){!1===$.isPlainObject(t.auth.modules)&&(t.auth.modules={});var r=0;t.auth.modules[e+(o||"")]?((r=t.auth.modules[e+(o||"")])<2&&(o||"")===auth.guid&&(r=2),r>=(n||0)&&i(false)):t.postXT({url:t.url("auth"),data:{module:e,person_guid:o||""},success:function(s){r=s[e],t.auth.modules[e+(o||"")]=r,r<2&&(o||"")===t.auth.person_guid&&(r=2),r>=(n||0)&&i(false)},error:function(e){t.failure.call(this,e)}})},t.auth.locale="de",t.ocms_prepauth=function(e,n,o){t.postXT({url:t.url("auth"),data:{fn:"csv",modules:e,person_guid:n||""},success:function(e){t.ocms_regauth(e)},error:function(e){t.failure.call(this,e)},complete:function(){o()}})},t.ocms_regauth=function(t){$.each(t||{},(function(t,e){auth.modules[t]=parseInt(e)}))},t.init=function(e){var n="string"==typeof e?e:(e.data||{}).fn||"";""!==n&&("home"===n?($cfr(),$lfr(),$("#topbar").ocmsmenu([],!0),$("#activemodule").text($t.ov),t.ov.call($("#contentframe"))):($cfr(),$lfr(),$("#topbar").ocmsmenu([]),t.postXT({url:t.url(n+"/auth"),success:function(e){void 0===t[n]&&(t[n]={}),t[n].auth=e,e.manage>0&&t.getScript({module:n,script:["web/imdl",n,t.auth.locale||"de","js"].join("."),css:["web/imdl",n,"css"].join("."),condition:"function"!=typeof t[n].init2},(function(){t[n].init2()}))},error:function(){$("#contentframe").empty()}})))},t.menuarray=function(t){this.array=[],this.sep=function(){this.length>0&&"separator"!==this.array[array.length-1].fnc&&this.push({fnc:"separator"})},this.push=function(t){return void 0===t?null:(!0===Array.isArray(t)?Array.prototype.push.apply(this.array,t):"object"==typeof t&&this.array.push(t),t)},this.unshift=function(t){return void 0===t?null:(!0===Array.isArray(t)?Array.prototype.unshift.apply(this.array,t):"object"==typeof t&&this.array.unshift(t),t)},this.push(t)},t.menu=function(e,n){e=e||[];var o=$(this).removeClass("vis");if(!0===bool(n,!0)&&!1===o.is("#mainmenu")&&o.empty(),!1===bool(n,!1)&&o.is("#sidebar,#topbar")&&(e.unshift({id:"sbctrl",glyph:"glyphicon-th-list",aclass:"fbtn",fnc:function(){$lf()}}),$lf(0)),0===(e||[]).length)o.empty().addClass("hd");else{o.removeClass("hd");var i=!0===o.is("nav")?o:o.children("nav");1!==i.length&&(i=$("").tC("nv",o.is("#sidebar")).tC("ctxt",o.is("#topbar")).appendTo(o));var r,s=$$.ul().appendTo(i),a=function(t,e){var n=$(this).addClass("dropdown submenu");t.append($$.sc("caret dd")).addClass("dds dropdown-toggle").attr({"aria-expanded":"false"}),""!==(e.ico||"")&&t.prepend($$.sc("ico "+e.ico));var o=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(n);$.each(e.itm||[],(function(t,e){r.call(o,e)}))},c=function(t){$(this).tC("disabled","boolean"==typeof t.disabled?t.disabled:"string"==typeof t.disabled&&"subs"===t.disabled&&0===(t.itm||[]).length)};r=function(e){var n,o=$$.li({id:e.id}).attr(e.attr||{}).addClass(e.lclass).appendTo($(this)),i="string"==typeof e.fnc&&""!==e.fnc?e.fnc.split(":")[0]:"";""!==i&&"init"!==i?o.attr("role",i).appendIf($$.s(e.lbl),""!==ne(e.lbl)):(n=$$.a({class:"on",role:"button"}).addClass(e.aclass).appendTo(o).append($$.s(e.lbl)),c.call(n,e),(e.itm||[]).length>0&&a.call(o,n,e),n.click($nuf),"function"==typeof e.fnc?n.click(e.data||{},e.fnc):"init"===i&&n.click($.extend({},e.data||{},{fn:e.fnc.split(":")[1]}),t.init))},$.each(e,(function(e,n){var o,i=$$.li({id:n.id}).attr(n.attr||{}).addClass(n.lclass),a="string"==typeof n.fnc&&""!==n.fnc?n.fnc.split(":")[0]:"";if(""!==a&&"init"!==a)i.attr("role",a).appendIf($$.s(n.lbl),""!==ne(n.lbl));else{if(o=$$.a({class:"on",role:"button"}).addClass(n.aclass).appendTo(i),c.call(o,n),""!==(n.lbl||"")&&o.append($$.s(n.lbl)),""!==(n.ico||"")&&o.prepend($$.sc("ico "+n.ico)),""!==(n.glyph||"")&&o.prepend($$.sc("glyphicon "+n.glyph)),(n.itm||[]).length>0){i.addClass("dropdown"),o.append($$.sc("caret dd")).addClass("dds dropdown-toggle").attr({"aria-expanded":"false"});var l=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(i);$.each(n.itm||[],(function(t,e){r.call(l,e)}))}(n.sel||[]).length>0||(o.click($nuf),"function"==typeof n.fnc?o.click(n.data||{},n.fnc):"init"===a&&o.click($.extend({},n.data||{},{fn:n.fnc.split(":")[1]}),t.init))}i.appendTo(s)})),i.activatemenu()}},t.easytbl=(t,e)=>{e=e||{};let n=$$.tbl().addClass(e.class).css("border-collapse","collapse"),o=($$.tbody(n),!0===bool(e.frame,!1)?{padding:"5px",border:"1px solid #727272"}:{});if(!0===Array.isArray(e.header)){let t=$$.thead(n);$.each(e.header,((n,i)=>$$.th(t).css(e.cellcss||o).rwText(i)))}else if(!0===bool(e.header,!1)&&(t||[]).length>0){let i=$$.thead(n);$.each(Object.keys(t[0]),((t,n)=>$$.th(i).css(e.cellcss||o).rwText(n)))}return $.each(t||[],((t,i)=>{let r=$$.tr();$.each(i,((t,n)=>{n=n||"";let i=$$.td(r).css(e.cellcss||o);n instanceof jQuery?i.append(n):"string"==typeof n&&("<"===n.substring(0,1)?i.append(n):i.text(n))})),n.append(r)})),n},t.dlgtbl=(e,n,o)=>{o=o||{};let i=t.easytbl(e,o);t.dlg(i,$.extend({title:n},o))},t.dlg=function(t,n){n=n||{};let o=$("body > .modal").length>0,i=t=>typeof n[t],r=t=>"function"===i(t);if(!0===bool(n.exclusive,!0)&&!0===o)return void alert($t.dbldlg||"Es ist bereits ein Dialog geöffnet");let s=$$.dc("modal",$("body")),a=$$.dc("modal-dialog",s);!1===isNaN(n.zindex)?s.css("zIndex",n.zindex):!0===o&&s.css("zIndex",parseInt($("body > .modal:last").cssValue("zIndex"))+200),!1===isNaN(n.zindex_min)&&s.cssValue("zIndex")').appendTo(d)),""!==ne(n.title)&&(c=$$.dc("modal-header",d),$("

      ").text(n.title).appendTo(c));let p=$$.dc("modal-body",d),f=$$.dc("modal-footer",d);t instanceof jQuery==!0&&p.append(t);let m=function(t){t&&"function"==typeof t.stopPropagation&&t.stopPropagation(),a.removeClass("in"),!0===r("closing")&&n.closing.call(d),p.hide().emptyWithEditors(),s.remove(),!0===r("close")&&n.close.call(d)};if(d.find(":input[required]").length>0&&($$.dc("note_required",f).append($$.sc("ind_required","*")).append($$.s($t.t1||"Eingabe erforderlich")),$$.dc("note_invalid",f).append($$.s($t.t2||"Bitte überprüfen Sie Ihre Eingaben im Formular."))),!0===r("cancel")){$$.bbtn(n.cancelbutton||"Abbrechen","cancel").attr({type:"button",role:"cancel"}).appendTo(f).click((function(t){n.cancel.call(d,t);t.stopPropagation(),m()}))}if(!0===r("confirm")){let t=$$.bbtn(n.button||"OK","confirm").attr({type:!0===bool(n.form,!1)?"submit":"button",role:"confirm"}).appendTo(f);!0===h?(d.submit((function(t){try{n.confirm.call(d,t)}finally{t.preventDefault()}return!1})),d.on("modal_submit",(function(){n.confirm.call(d,e)}))):(t.click((function(t){n.confirm.call(d,t);t.stopPropagation()})),d.on("modal_submit",(function(){t.click()})))}else!0===h&&d.submit((function(t){return t.preventDefault(),!1}));return d.on("modal_close",(function(){m()})),l.click(m),!0===r("opening")&&n.opening.call(d),a.addClass("in"),ne(n.mode).indexOf("maxbody")>-1&&p.css("min-height",(u.height()-c.outerHeight()-f.outerHeight()).toString()+"px"),!0===r("open")&&n.open.call(d),{hd:c,bdy:p,ft:f,ct:u,dlg:a,c:d}},t.mform=function(e){let n=$$.dc("form-body"),o=Array.isArray(e)?e:e instanceof fields_definition?e.fields:[];return $.each(o||[],(function(e,o){let i=o.type||"";if("ignore"===i)return!0;let r=$$.dc("form-group",n),s=o.id||"dlg_"+(o.name||"")+("html"===o.type?"_"+(65536*(1+Math.random())||0).toString(16).substr(9):""),a=$$.lbl(o.label||o.name,{for:s}).appendTo($$.dc("form-itm",r)),c=$$.dc("form-itm",r),l=$$.i({id:s,name:o.name,placeholder:o.placeholder,type:o.type});switch(i){case"email":o.pattern=ne(o.pattern,"[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$");break;case"url":o.pattern=ne(o.pattern,"https?://.+");break;case"number":o.pattern=ne(o.pattern,"[-+]?[0-9]*[.,]?[0-9]*"),l.attr("step",o.precision||"any"),l.attr("data-format","float");break;case"integer":case"int":o.pattern=ne(o.pattern,"[-+]?[0-9]*"),l.attr("type","number"),l.attr("data-format","integer");break;case"date":if(""!==ne(o.pattern,$t.datepattern)&&(o.pattern=ne(o.pattern,"("+$t.datepattern+")|([0-9]{4}.(0[1-9]|1[012]).(0[1-9]|1[0-9]|2[0-9]|3[01]))")),""!==ne(o.placeholder,$t.dateplaceholder)&&l.attr("placeholder",ne(o.placeholder,$t.dateplaceholder)),"string"==typeof o.value){var u=o.value.substr(0,10);o.value="date"!==l.prop("type")?fdt(u+"T00:00:00",ne(o.dateformat,$t.dateformat)):u}l.attr("data-format","date:"+ne(o.dateformat,$t.dateformat)+";yyyy-MM-dd");break;case"datetime":l.attr("type","datetime-local"),""!==ne(o.pattern,$t.datetimepattern)&&(o.pattern=ne(o.pattern,"("+$t.datetimepattern+")|([0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])\\s([0-5][0-9]):([0-5][0-9]))")),""!==ne(o.placeholder,$t.datetimeplaceholder)&&l.attr("placeholder",ne(o.placeholder,$t.datetimeplaceholder)),"string"==typeof o.value&&"T"===o.value.substr(10,1)&&(o.value="datetime"!==l.prop("type").substr(0,8)?fdt(o.value,ne(o.datetimeformat,$t.datetimeformat)):o.value),l.attr("data-format","datetime:"+ne(o.datetimeformat,$t.datetimeformat)+";yyyy-MM-dd HH:mm:ss");break;case"hidden":r.addClass("hd");break;case"html":case"text":l=$$.txt({id:s,name:o.name,placeholder:o.placeholder,type:o.type}),l.tC("tinymce","html"===o.type);break;case"bool":case"boolean":o.url=[{value:"true",label:($t||{}).true||"Yes"},{value:"false",label:($t||{}).false||"No"}],"boolean"==typeof o.value&&(o.value=o.value?"true":"false");case"select":l=$$.sel({id:s,name:o.name,type:o.type}),!1===bool(o.required,!1)&&$$.eOpt().appendTo(l);try{var d=function(t){!0===Array.isArray(t)&&$.each(t,(function(t,e){"string"==typeof e?$$.opt(e,e).appendTo(l):!0===Array.isArray(e)?$$.opt(e[0],e[1]).appendTo(l):"object"==typeof e&&$$.opt(e.value,e.label||e.text).appendTo(l)}))};!0===Array.isArray(o.url)?d(o.url):"function"==typeof o.url?o.url.call(l):"string"==typeof o.url&&t.postXT({url:o.url,success:d})}catch(t){$.noop()}break;default:""!==ne(o["max-length"])&&l.attr("max-length",o["max-length"])}""!==ne(o.pattern)&&l.attr("pattern",o.pattern),l.val(o.value).change(),l.change((function(){$(this)[0].setCustomValidity("")})),l.addClass("form-control").prop("required",bool(o.required,!1)).prop("readonly",bool(o.readonly,!1)).appendTo(c),!0===bool(o.required,!1)&&a.append($$.sc("ind_required","*")),"object"==typeof o.attr&&l.attr(o.attr),"object"==typeof o.prop&&l.prop(o.prop),"string"==typeof o.class&&l.addClass(o.class),"function"==typeof o.change&&(l.change(o.change),!0===bool(o.applychange,!1)&&void 0!==o.value&&l.change()),""!==(o.note||"")&&$$.dc("form-note",c).rwText(o.note),"function"==typeof o.complete&&o.complete.call(l)})),n},t.initMCE=function(t,e){t=$(t),e=e||{};try{let n={target:t[0],inline:!1,width:e.width||"100%",statusbar:!1,document_base_url:window.location.origin+"/",content_style:"ph:before {content: '«'; color: #BBB; font-style:italic; } ph:after {content: '»'; color: #BBB; font-style:italic; } ph { color: #AAA; font-style:italic; }",relative_urls:!1,remove_script_host:!1};!0===bool(e.hidemenu,!1)&&(n.menubar=!1,n.menu={}),!0===bool(e.hidetoolbar,!1)&&(n.toolbar=!1),$.extend(n,e||{}),tinymce.init(n)}catch(t){alert(t.message)}},t.dlgform=function(e,n){n=n||{};let o,i=$$.dc("frm").append(t.mform(e||[]).addClass("stacked"));n.addcontent instanceof jQuery&&i.append(n.addcontent),"function"==typeof n.submit?o=n.submit:"function"==typeof n.success&&(o=function(e){var o=$(this).ldng(1),i=$.extend({loginaccount:t.auth.account||""},o.serializeObject(bool(n.checkvalidity,!0),{typedvalues:bool(n.typedvalues,!1)}));""!==(n.url||"")?t.postXT({url:n.url,data:i,success:function(t){n.success.call(this,t),o.trigger("modal_close")},error:function(){alert($t.l17)},complete:function(){o.ldng(0)},timeout:6e4}):(n.success.call(this,i),o.trigger("modal_close"))});let r={form:!0,title:n.title||"",button:n.button||$t.submit,confirm:o,size:n.size||[500,600],open:function(){let e=$(this).find(".tinymce");e.length>0&&t.initMCE(e,n.tinymce||{})}};return t.dlg.call(this,i,r)},t.login.dlg=function(e){e=e||{};let n=[{name:"userinfo",label:$t.l1,type:"string",value:t.auth.login,change:t.login.uichange,required:!0},{name:"userlogin",type:"hidden",required:!0,value:t.auth.login},{name:"username",type:"string",label:$t.l4,required:!0,readonly:!0,placeholder:$t.l5,value:t.auth.fullname_rev},{name:"userpass",type:"password",label:$t.l3,required:!0,placeholder:$t.l3}];""===(t.auth.account||"")&&n.unshift({id:"dlg_loginaccount",name:"loginaccount",type:"string",required:!0,value:t.auth.account});let o=$$.dc("frm").append(t.mform(n).addClass("stacked")),i=t.dlg.call(this,o,{form:!0,title:$t.l0,button:$t.submit,confirm:function(n){var o=$(this).ldng(1),i=$.extend({loginaccount:t.auth.account||""},o.serializeObject());t.postXT({url:"/vt/login",data:i,success:function(n){""!==((n||{}).login||"")&&(o.trigger("modal_close"),t.auth=n,"object"==typeof e.ajo&&(e.ajo.islogin,$.ajax(e.ajo)))},error:function(){alert($t.l17)},complete:function(){o.ldng(0)},timeout:6e4})},size:[500,600]}),r=$$.dc("modal-content").css("height","auto").attr("novalidate","true").append($$.dc("modal-header").appendIf($("

      ").text(t.auth.accountname),""!==(t.auth.accountname||"")).append($("

      Vereinsmanager

      ")));i.dlg.prepend(r)},t.addNoEntryInfo=function(t){$(this).append($$.dc("noentryinfo").text(t||$t.t11))}}($ocms),Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),Element.prototype.closest||(Element.prototype.closest=function(t){var e=this;do{if(Element.prototype.matches.call(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}),function(t,e){var n,o;"object"==typeof window&&(window[t]=(n=function(t){var e=window,n=document.body,o=document.documentElement,i=Math.max(0,e.pageXOffset||o.scrollLeft||n.scrollLeft||0)-(o.clientLeft||0),r=Math.max(0,e.pageYOffset||o.scrollTop||n.scrollTop||0)-(o.clientTop||0);return{x:t?Math.max(0,t.pageX||t.clientX||0)-i:0,y:t?Math.max(0,t.pageY||t.clientY||0)-r:0}},(o=function(t,e){t&&t instanceof Element&&(this._container=t,this._options=e||{},this._clickItem=null,this._dragItem=null,this._showDragItem="boolean"!=typeof this._options.dragItem||!1!==this._options.dragItem,this._hovItem=null,this._sortLists=[],this._click={},this._dragging=!1,this._dragHandleClass=this._options.dragHandleClass||"",this._parentident=this._options.parentident||"",this._swapdone="function"==typeof this._options.swapdone?this._options._swapdone:null,this._container.setAttribute("data-is-sortable",1),this._container.classList.add("sortable"),this._container.style.position="static",window.addEventListener("mousedown",this._onPress.bind(this),!0),window.addEventListener("touchstart",this._onPress.bind(this),!0),window.addEventListener("mouseup",this._onRelease.bind(this),!0),window.addEventListener("touchend",this._onRelease.bind(this),!0),window.addEventListener("mousemove",this._onMove.bind(this),!0),window.addEventListener("touchmove",this._onMove.bind(this),!0))}).prototype={constructor:o,toArray:function(t){t=t||"id";for(var e=[],n="",o=0;oo.left&&eo.top&&n-1)&&e.className.indexOf("nosort")<0)&&(t.preventDefault(),this._dragging=!0,this._click=n(t),this._makeDragItem(e),this._onMove(t),!0)}t&&!1===e.call(this,t.target)&&""!==this._parentident&&t.target.closest(this._parentident)&&e.call(this,t.target.closest(this._parentident))},_onRelease:function(t){this._dragging=!1,this._trashDragItem()},_onMove:function(t){if(this._dragItem&&this._dragging){t.preventDefault();var e=n(t),o=this._container;!0===this._showDragItem&&this._moveItem(this._dragItem,e.x-this._click.x,e.y-this._click.y);for(var i=0;i0?a.mousedown(c).addClass("dctrl"):s.mousedown(c).addClass("dctrl"),t(this)}}(jQuery),$(document).ready((function(){$("html").click((function(t){$nuf()})),$("#listframe").click((function(t){t.stopPropagation(),$nuf()})),$("#mainmenu").ocmsmenu($ocms.ocmsmenu),$("#mainmenu").activatemenu()})),$.extend($t,{m_inv:"Rechnungen",m_req:"Aufträge",m_rep:"Berichte",m_todo:"ToDos",m_bcd:"BankBuchungen",rsp:"Passwort ändern",pnm:"Die Passwörter stimmen nicht überein",cps:"Das neue Passwort wurde gespeichert.",pwr:"Bitte wählen Sie ein starkes Passwort (min 8 Zeichen, davon jeweils min 2 Zahlen, kleine und große Buchstaben, Sonderzeichen sind optional).",smsc:"Sie beötigen für diese Funktion einen SMS-Code.\nSoll dieser nun versandt werden?",wdc:"Doppelt klicken, um die Box zu aktualisieren.",wdg:{}}),$t.rspf={sms:"Der SMS-Code konnte nicht bestätigt werden",valid:"Das alte Passwort ist nicht korrekt",requirements:"Das Passwort entspricht nicht den Anforderungen.\n"+$t.pwr},$fd={rsp:new fields_definition("","",[{name:"opw",label:"aktuelles Passwort",type:"password",required:!0,attr:{"auto-complete":"current-password"}},{name:"npw",label:"neues Passwort",type:"password",required:!0,pattern:"(.{6,})",attr:{"auto-complete":"new-password"}},{name:"npwc",label:"neues Passwort (Bestätigung)",type:"password",required:!0,attr:{"auto-complete":"new-password"},note:$t.pwr},{name:"code",label:"SMS-Code",type:"string",required:!0,attr:{"auto-complete":"one-time-code"}}])},$ocms.init=function(t){var e="string"==typeof t?t:(t.data||{}).fn||"";""!==e&&("home"===e?($cfr(),$lfr(),$("#topbar").ocmsmenu([],!0),$("#activemodule").text($t.ov),$fis.ov()):($cfr(),$lfr(),$("#topbar").ocmsmenu([]),$ocms.postXT({url:$ocms.url(e+"/auth"),success:function(t){void 0===$ocms[e]&&($ocms[e]={}),$ocms[e].auth=t,t.manage>0&&$ocms.getScript({module:e,script:["/web/fis",e,$ocms.auth.locale||"de","js"].join("."),css:["/web/fis",e,"css"].join("."),condition:"function"!=typeof $ocms[e].init2},(function(){$ocms[e].init2()}))},error:function(){$("#contentframe").empty()}})))};var $fis={auth:{},db:function(){$("#mainmenu_activemodule").text($t.ov);let t=$(this).empty(),e=$$.d({id:"dashboard_frame"}).appendTo(t);$ocms.postXT({url:$ocms.url("wdg/my"),success:function(t){$.each(t,(function(t,n){var o=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$ocms.wdg.call(o,{wdg:n})}))},loading:e})},ValidateEmail:function(t){return!!/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(t)},cf:t=>{let e=$("#contentframe");return!0===bool(t,!1)&&e.empty().rC("hd"),e},lf:t=>{let e=$("#listframe");return!0===bool(t,!1)&&e.empty().aC("hd").rC("fix"),e},frm_edit:function(t){let e=$fis.cf(!1),n=e.children(".cfrm"),o=e.children(".edit_frm");return n.length<1?n=$$.dc("cfrm hd").prependTo(e):!0===bool(t,!1)&&n.empty(),o.length<1&&(o=$$.dc("edit_frm").insertAfter(n)),o.empty()},frm_list:function(t,e){let n=$fis.cf(!1),o=n.children(".cfrm"),i=n.children(".list_frm");return o.length<1?o=$$.dc("cfrm hd").prependTo(n):!0===bool(t,!1)&&o.empty(),!0===bool(e,!1)&&n.children(".edit_frm").remove(),i.length<1&&(i=$$.dc("list_frm").appendTo(n)),i.empty()},lfm:()=>{let t=$fis.lf(!1),e=t.children(".lfrm");return e.length<1&&(e=$$.dc("lfrm").prependTo(t)),e},getAuth:(t,e)=>new Promise(((n,o)=>{$fis.auth[t]&&!1===bool(e,!1)?n($fis.auth[t]||-1):$ocms.postXT({url:$ocms.url("auth"),data:{module:t},success:e=>{$fis.auth[t]=e.auth||-1,n($fis.auth[t]||-1)},error:()=>{o()}})})),prepAuth:t=>new Promise(((e,n)=>{$ocms.postXT({url:$ocms.url("auth"),data:{module:t,array:1},success:t=>{$.extend($fis.auth,t||{})},complete:()=>{e()}})})),isAuth:(t,e)=>($fis.auth[t]||-1)>=(e||1),resetPass:function(t,e){confirm($t.smsc)&&($ocms.postXT({url:$ocms.url("account/sms"),data:{fn:"pwc"}}),$ocms.dlgform($fd.rsp.clone(),{title:$t.rsp||"",submit:function(t){var e=$(this).ldng(1),n=$.extend({loginaccount:$ocms.auth.account||""},e.serializeObject(!0,{typedvalues:!0}));(n.npw||"")!==(n.npwc||"")?e.find('input[name="npw"]:first')[0].setCustomValidity($t.pnm):$ocms.postXT({url:$ocms.url("account/changepassword"),data:n,success:function(t){alert($t.cps),e.trigger("modal_close")},error:function(t){alert($t.rspf[t.getResponseHeader("x-ocms-std")])},complete:function(){e.ldng(0)},timeout:6e4})}}))},wdg:function(t){let e=$(this).empty();$ocms.postXT({url:$ocms.url("wdg/one"),data:{short_name:t.wdg},timeout:9e4,success:function(n,o,i){let r=t.wdg,s=n[r];if(!s)return void e.ldng(0);let a=$.inArrayRegEx("dblwidth",s.rendering_options)>-1,c=$.inArrayRegEx("tiny",s.rendering_options)>-1;e.toggleClass("dbl",a&&!c).toggleClass("tny",c);$$.dc("wdg_hd",e,{title:ne(s.description,$t.wdc)}).toggleClass("dbl",a).text(ne(s.name,t.wdg)).dblclick((function(t){t.stopPropagation(),$fis.wdg.call(e,{wdg:r})}));let l=$$.dc("wdg_cnt",e).toggleClass("dbl",a).hide(),u=$.inArrayRegEx("bgcolor",s.rendering_options);switch(u>-1&&l.css("backgroundColor",s.rendering_options[u].toString().right(":")),s.type){case"table":var d=$$.tblset({},l),h=$$.tr().appendTo(d.hd),p=$t.wdg[r.indexOf("wdg_ev_")>=0?"wdg_ev_":r]||{};$.each(s.columns,(function(t,e){var n=p[e]?p[e].label:e;$$.th().text(n).appendTo(h)})),$.each(s.data,(function(t,e){var n=$$.tr().appendTo(d.bdy);$.each(s.columns,(function(t,o){var i=$$.td().appendTo(n);e[o]instanceof Date||!0===$ocms.isJSONDateString(e[o])?i.text(fdt(e[o],$t.dateformat)):i.rwText(e[o])}))})),$.inArray("firstrow_bold",s.rendering_options)>-1&&h.nextAll("tr:first").css("font-weight","bold");break;case"ind":$$.dc("ind",l).addClass("sts_"+(s.data.status||"")).append([$$.dc("ind").text(s.data.value),$$.lbl(s.data.label)]);break;case"image_url":l.css("background","url('"+s.url+"') no-repeat center center transparent");break;case"image_base64":l.css("background","url('data:image/png;base64,"+s.image+"') no-repeat center center transparent");break;case"html":if(l.html(s.html),$.inArray("reload_10min",s.rendering_options)>-1){var f=l.find("iframe");setTimeout((function(){f.attr("src",(function(t,e){return e}))}),6e5)}}$.inArray("reload_30min",s.rendering_options)>-1&&"html"!==s.type&&setTimeout((function(){$fis.wdg.call(e,{wdg:r})}),18e5),l.slideDown(150)},error:function(t){e.slideUp(150),$fis.failure.call(this,t)},complete:function(){e.ldng(0)}})},ov:function(){$fis.lf(!0);let t=$("#contentframe").empty(),e=$$.d({id:"dashboard_frame"}).appendTo(t);$ocms.postXT({url:$ocms.url("wdg/my"),success:function(t){$.each(t,(function(t,n){var o=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$fis.wdg.call(o,{wdg:n})}))},loading:e})}};$fis.notifications={connection:null,init:function(){"undefined"!=typeof signalR&&null===this.connection&&$ocms.auth.useraccount_id&&(this.ensureFrame(),this.connection=(new signalR.HubConnectionBuilder).withUrl("/notifications").withAutomaticReconnect().build(),this.connection.on("notification",(t=>{this.push(t)})),this.connection.onclose((()=>{console.warn("Notification connection closed; retrying in 5s."),this.connection=null,setTimeout((()=>this.init()),5e3)})),this.start())},start:function(){this.connection.start().catch((t=>{console.warn("Notification connection failed to start; retrying in 5s.",t),this.connection=null,setTimeout((()=>this.init()),5e3)}))},ensureFrame:function(){$("#notification_frame").length<1&&$("
      ",{id:"notification_frame"}).appendTo($("footer:first").length?"footer:first":"body")},push:function(t){this.ensureFrame(),t=t||{};let e=$("
      ",{class:"notification_item"}).addClass((t.severity||"info").toLowerCase()).append($("
      '),n=e.find(".form-body"),i=null;e.find("form").submit((function(t){t.preventDefault();var o=$(this).serializeObject(!0),r=null===i,s=r?"spwc":"spw";return $ocms.postXT.call(this,{url:$ocms.url(s),data:o,complete:function(){r?(n.append('
      Ihnen wurde ein Code per SMS zugesandt.
      Bitte tragen Sie den hier ein:
      '),i=$('
      ').appendTo(n)):(alert($t.l13),e.remove())},error:()=>{}}),!1})),e.find(".modal-close").click((function(){e.remove()}));var o=[];$.each($t.l7a.split("\n"),((t,e)=>{Array.prototype.push.apply(o,[$("
      "),$("").text(e)])})),e.find(".modal-note").append($('').text($t.alert)).append(o),e.appendTo("body"),setTimeout((function(){$(".modal").find('input[name="lastname"]').focus()}),600)}};var $$={s:function(t){return $("").text(t)},br:function(){return $("
      ")},sc:function(t,e){return $("").addClass(t).text(e)},td:function(t,e){var n=$("");return t instanceof jQuery==!0?n.appendTo(t):"object"==typeof t?n.attr(t):"string"==typeof t&&n.text(t),"object"==typeof e?n.attr(e):"string"==typeof e&&n.text(e),n},th:function(t,e){var n=$("");return t instanceof jQuery==!0?n.appendTo(t):"object"==typeof t?n.attr(t):"string"==typeof t&&n.text(t),"object"==typeof e?n.attr(e):"string"==typeof e&&n.text(e),n},tdc:function(t,e,n){return $$.td(e,n).addClass(t)},td2:function(t){var e=$('');return"string"===$.type(t)?e.text(t):t instanceof jQuery?e.append(t):"function"==typeof t?t.call(e):e.html(" "),e},td3:function(t){var e=$('');return"string"===$.type(t)?e.text(t):t instanceof jQuery?e.append(t):"function"==typeof t?t.call(e):e.html(" "),e},tdtr:function(t,e){var n=$$.tr().appendTo(e);return t instanceof jQuery==!0||"string"==typeof t?t.appendTo($$.td().appendTo(n)):!0===Array.isArray(t)&&$.each(t,(function(t,e){$(e).appendTo($$.td().appendTo(n))})),n},tr:function(t,e){var n=$("");return t instanceof jQuery==!0?n.appendTo(t):"object"==typeof t&&n.attr(t),"object"==typeof e&&n.attr(e),n},trc:function(t,e){var n=$("").addClass(t);return e instanceof jQuery==!0?n.appendTo(e):"object"==typeof e&&n.attr(e),n},d:function(t){return $("
      ").attr(t||{})},dc:function(t,e,n,i){var o=$("
      ").addClass(t);return e instanceof jQuery==!0?o.appendTo(e):"object"==typeof e?o.attr(e):"function"==typeof e?o.click(e):"string"==typeof e&&o.text(e),"string"==typeof n?o.text(n):"object"==typeof n?o.attr(n):"function"==typeof n&&o.click(n),"string"==typeof i?o.text(i):"object"==typeof i?o.attr(i):"function"==typeof i&&o.click(i),o},df:function(t){return $("
       
      ").attr(t||{})},opt:function(t,e,n){var i=$("");return"string"==typeof t?i.attr("value",t):"object"==typeof t&&i.attr(t),"string"==typeof e?i.text(e):"object"==typeof e&&i.attr(e),"object"==typeof n&&i.attr(n),i},eOpt:function(t){var e=$('');return t&&e.attr("selected","selected"),e},tbl:function(t){return $("
      ").attr(t||{})},tblc:function(t){return $("
      ").addClass(t)},thead:function(t){let e=$("");return t instanceof jQuery&&e.prependTo(t),e},tbody:function(t){let e=$("");return t instanceof jQuery&&e.appendTo(t),e},tblset:function(t,e){let n=$$.tbl(t||{});return e instanceof jQuery&&e.append(n),{tbl:n,hd:$$.thead().appendTo(n),bdy:$$.tbody().appendTo(n)}},i:function(t){return $("").attr(t||{})},img:function(t,e){return $("").attr("src",t).attr(e||{})},sel:function(t){return $("").attr(t||{})},btn:function(t){return $("").attr(t||{})},a:function(t){return $("").attr(t||{})},li:function(t){return $("
    • ").attr(t||{})},ul:function(t){return $("
        ").attr(t||{})},nav:function(t){return $("").attr(t||{})},lbl:function(t,e){var n=$("");return"string"==typeof t&&n.text(t),"object"==typeof t?n.attr(t):"object"==typeof e&&n.attr(e),n},txt:function(t){return $("").attr(t||{})},0:function(t,e){return $("<"+t+">").attr(e||{})},bbtn:function(t,e){return $$.btn({type:"button",class:"btn"}).addClass(e).text(t)},svg:t=>$(document.createElementNS("http://www.w3.org/2000/svg",t))};function getMonday(t){var e=(t=new Date(t)).getDay(),n=t.getDate()-e+(0==e?-6:1);return new Date(t.setDate(n))}function $lf(t){var e=void 0===t?null:"number"==typeof t&&1!==t||"boolean"==typeof cl&&!1===t;return $("#listframe").tC("hd",e).is(".hd")}function $nuf(t){if(t&&t.stopPropagation(),!$(this).is(".disabled")){var e=function(t){t.removeClass("vis").find("li.dropdown").removeClass("open").removeClass("vis").attr("aria-expanded","false")},n=$(this).parent("li.dropdown");if(n.length>0){n.tC("open"),navs=!0===n.is(".open")?"true":"false",n.attr("aria-expanded",navs);var i=n.closest("nav");i.find("li.dropdown").not(n.parentsUntil("nav")).not(n).removeClass("open").attr("aria-expanded","false"),!1===n.is(".open")&&n.find("li.dropdown").removeClass("open").attr("aria-expanded","false"),e($("nav").not(i))}else e($("nav"))}}function $tbr(){return $lf(0),$("#topbar").ocmsmenu([])}function $lfr(){return $("#sidebar").empty(),$("#listframe").removeClass("fix").addClass("hd").empty()}function $cfr(){return $tbr(),$("#contentframe").empty()}function jObj(t,e){let n={};if("{"===(t||"").substr(0,1))try{n=JSON.parse(t)}catch(t){n={}}return n[e]||""}function string(t,e){var n,i=t||"";return $.each(e||[],(function(t,e){n=new RegExp("\\{"+t.toString()+"\\}","ig"),i=i.replace(n,e)})),i}function init_tooltip(t){var e=!0===("boolean"==typeof t&&t)&&"mouse";$("[title]").qtip({position:{target:e,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},effect:!1}),$("div.tooltiptext").each((function(){$(this).filter(":not(:empty)").parent().qtip({suppress:!1,content:{text:$(this).clone()},position:{target:e,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden}})}))}String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")},String.prototype.left=function(t){if("string"===$.type(t)){var e=this.indexOf(t);return e>0?this.slice(0,e):""}return this.substring(0,t)},String.prototype.right=function(t){if("string"===$.type(t)){var e=this.indexOf(t);return e>0?this.substring(this.length-e):""}return this.substring(this.length-t)},Array.prototype.move=function(t,e){if(e>=this.length)for(var n=e-this.length;1+n--;)this.push(void 0);return this.splice(e,0,this.splice(t,1)[0]),this},function(t){t.fn.appendToIf=function(e,n){var i=t(this),o="function"==typeof n?n(i):n;return!0===("boolean"!=typeof o||o)&&i.appendTo(e),i},t.fn.appendIf=function(e,n){var i=t(this),o="function"==typeof n?n(i):n;return!0===("boolean"!=typeof o||o)&&i.append(e),i},t.fn.rwText=function(e,n,i){var o=t(this).empty();i=t.extend({wrap:!0},i);var r=!0===Array.isArray(e)?e:(null==e?"":String(e)).split("\n");return t.each(r,(function(t,e){""!==(e||"")&&(t>0&&o.append($$.br()),o.append(!0===i.wrap?$$.s(e):e))})),n&&o.attr("title",n),o},t.fn.loadSel=function(e,n,i){if("SELECT"===t(this).prop("tagName").toUpperCase()){var o=t(this);$ocms.postXT.call(this,{url:e,data:n||{},success:function(e){t.each(e,(function(){o.append($$.opt(e.value,e.text))}))},complete:function(){o.ldng(0),"function"==typeof i&&i.call(o)}})}},t.fn.emptyWithEditors=function(e){var n=t(this);return n.find(":input.tinymce").each((function(e,n){try{var i=tinymce.get(t(n).attr("id"));i&&i.remove()}catch(e){t.noop()}})),n.empty()},t.fn.cssValue=function(t){if(this.length>0){var e=this.css(t)||"";if(""===e)return 0;var n=/(^[\d\.]*)(\D{1,3}$)/gi.exec(e);return null!==n?"rem"===n[2]?$ocms.rpx(parseFloat(n[1])):parseFloat(n[1]):!1===isNaN(e)?parseFloat(e):0}return 0},t.fn.veryInnerHeight=function(){let e=e=>t(this).cssValue(e);return t(this).innerHeight()-e("padding-top")-e("padding-bottom")},t.fn.veryInnerWidth=function(){let e=e=>t(this).cssValue(e);return t(this).innerWidth()-e("padding-left")-e("padding-right")},t.fn.marginWidth=function(){let e=e=>t(this).cssValue(e);return e("margin-left")+e("margin-right")},t.fn.marginHeight=function(){let e=e=>t(this).cssValue(e);return e("margin-top")+e("margin-bottom")},t.inArrayRegEx=function(e,n,i){var o="regexp"===t.type(e)?e:new RegExp(e);if(!n)return-1;for(var r=i=i||0;r7){i=e.split(","),o=(n||(t<0?"rgb(0,0,0)":"rgb(255,255,255)")).split(",");var c=a(i[0].slice(4)),l=a(i[1]),u=a(i[2]);return"rgb("+(s((a(o[0].slice(4))-c)*r)+c)+","+(s((a(o[1])-l)*r)+l)+","+(s((a(o[2])-u)*r)+u)+")"}var d=(i=a(e.slice(1),16))>>16,h=i>>8&255,p=255&i;return"#"+(16777216+65536*(s((((o=a((n||(t<0?"#000000":"#FFFFFF")).slice(1),16))>>16)-d)*r)+d)+256*(s(((o>>8&255)-h)*r)+h)+(s(((255&o)-p)*r)+p)).toString(16).slice(1)},t.fn.IN=function(e){return t(this).fadeIn(400,e),t(this)},t.fn.OUT=function(e){return t(this).fadeOut(400,e),t(this)},t.fn.tooltip=function(e,n){var i=!0===("boolean"==typeof e&&e)&&"mouse",o="boolean"==typeof n&&n,r=t(this);return r.each((function(){var e=o?t(this).find(".tooltiptext"):t(this).children(".tooltiptext");t(e).length>0?e.each((function(){var e=t(this);t(this).filter(":not(:empty)").parent().qtip({suppress:!1,content:{text:e.clone()},position:{target:i,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},show:{effect:!1},hide:{effect:!1}}),e.remove()})):t(this).qtip({position:{target:i,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},effect:!1})})),r},t.fn.rC=function(e){return t(this).removeClass(e)},t.fn.aC=function(e){return t(this).addClass(e)},t.fn.tC=function(e,n){return t(this).toggleClass(e,n)}}(jQuery),function(t){t.fn.ocmsmenu=function(e,n){var i=t(this);return $ocms.menu.call(i,e,n),i},t.fn.activatemenu=function(){var e=t(this).filter("nav");return e.find("a").not(".on").addClass("on").click($nuf),e.find(".nav-btn").not(".on").addClass("on").click((function(e){e.stopPropagation();var n=t(this);t(n.attr("data-target")).tC(n.attr("data-toggle"))})),e}}(jQuery);class ObjectArray extends Array{isEmpty(){return 0===this[0].length}static get[Symbol.species](){return Array}filter(t){return"function"==typeof t?new ObjectArray(this[0].filter(t)):this}remove(t){if("function"!=typeof t)return this;{let e=this[0].findIndex(t);for(;e>-1;)this[0].splice(e),e=this[0].findIndex(t)}}sortBy(t){return"function"==typeof t&&this[0].sort(t),this}sortString(t){return this[0].sort(((e,n)=>{let i=(e[t]||"").toString().toUpperCase(),o=(n[t]||"").toString().toUpperCase();return console.debug(i.localeCompare(o)),i.localeCompare(o)})),this}sortNum(t){return this[0].sort(((e,n)=>{let i=e[t],o=n[t];return!0===isNaN(o)&&!1===isNaN(i)||io?1:0})),this}sum(t){return this[0].reduce(((e,n)=>e+(!0===isNaN(n[t])?0:n[t])),0)}groupBy(t){return this[0].reduce((function(e,n){let i=n[t];return e[i]||(e[i]=[]),e[i].push(n),e}),{})}each(t){if("function"==typeof t){let e=!1;this[0].forEach(((n,i,o)=>{if(!1===e){let r=t(n,i,o);"boolean"==typeof r&&!1===r&&(e=!0)}}))}}get toArray(){return this[0]}}class NumArray extends Array{sum(){return this.reduce(((t,e)=>t+e))}first(){return this[0]}last(){return this[this.length-1]}average(){return this.sum()/this.length}range(){let t=this.map((t=>t)).sort();return{min:t[0],max:t[this.length-1]}}static get[Symbol.species](){return Array}}$ocms.ocmsmenu=[{lbl:"",id:"m_home",ico:"glyphicon glyphicon-home",fnc:"init:home"},{fnc:"separator"}],function(t){t.multline=function(t){let e=t.split("\n"),n=$$.d();return $.each(e,((t,e)=>{n.append($$.s(e))})),n.html()},t.tooltip_hidden=function(t,e){$(this).remove(),e.rendered=!1},t.isJSONDateString=function(t){return"string"==typeof t&&/^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/.test(t)},t.failure=function(e){11110===(e.internalCode||-1)?t.login.dlg():alert($t.f1+"\n"+(e.internalText||""))},t.getScript=function(e,n){var i=[],o=[],r=function(t){return"string"==typeof t&&""!==(t||"")},s=function(t,e){!0===bool(e.condition,!0)&&(""!==(e.script||"")&&o.push({url:e.script,module:e.module||""}),!0===r(e.css||"")?i.push(e.css):!0===Array.isArray(e.css)&&Array.prototype.push.apply(i,e.css.filter(r)))};!0===r(e||"")?o.push(e):!0===Array.isArray(e)?$.each(e,s):"object"==typeof e&&""!==(e.script||"")&&s(0,e);let a=[];$.each(i,(function(t,e){""!==(e||"")&&a.push(loadCSS(e))}));let c=o.map((function(e,n){let i=e.url,r=e.module||"";if(""===r){let t=new Promise((function(t,e){try{!async function(){$.ajax({url:i,dataType:"script",success:function(){t(o)},error:function(){e(o)},timeout:3e4})}()}catch(t){console.debug(t.message+"%o",t)}}));return t}return t.loadmodule(r,i,e.alias)}));Promise.all(c).then(n)},t.loadmodule=function(e,n,i){let o=new Promise((function(o,r){!async function(){try{let s=(n.startsWith("/")||n.startsWith(".")?"":"/")+n;import(s).then((n=>{t[e]=n[i||"default"],o(e)})).catch((t=>{console.debug(t.message+"%o",t),r(e)}))}catch(t){console.debug(t.message+"%o",t)}}()}));return o},t.ocms_auth=function(e,n,i,o){!1===$.isPlainObject(t.auth.modules)&&(t.auth.modules={});var r=0;t.auth.modules[e+(i||"")]?((r=t.auth.modules[e+(i||"")])<2&&(i||"")===auth.guid&&(r=2),r>=(n||0)&&o(false)):t.postXT({url:t.url("auth"),data:{module:e,person_guid:i||""},success:function(s){r=s[e],t.auth.modules[e+(i||"")]=r,r<2&&(i||"")===t.auth.person_guid&&(r=2),r>=(n||0)&&o(false)},error:function(e){t.failure.call(this,e)}})},t.auth.locale="de",t.ocms_prepauth=function(e,n,i){t.postXT({url:t.url("auth"),data:{fn:"csv",modules:e,person_guid:n||""},success:function(e){t.ocms_regauth(e)},error:function(e){t.failure.call(this,e)},complete:function(){i()}})},t.ocms_regauth=function(t){$.each(t||{},(function(t,e){auth.modules[t]=parseInt(e)}))},t.init=function(e){var n="string"==typeof e?e:(e.data||{}).fn||"";""!==n&&("home"===n?($cfr(),$lfr(),$("#topbar").ocmsmenu([],!0),$("#activemodule").text($t.ov),t.ov.call($("#contentframe"))):($cfr(),$lfr(),$("#topbar").ocmsmenu([]),t.postXT({url:t.url(n+"/auth"),success:function(e){void 0===t[n]&&(t[n]={}),t[n].auth=e,e.manage>0&&t.getScript({module:n,script:["web/imdl",n,t.auth.locale||"de","js"].join("."),css:["web/imdl",n,"css"].join("."),condition:"function"!=typeof t[n].init2},(function(){t[n].init2()}))},error:function(){$("#contentframe").empty()}})))},t.menuarray=function(t){this.array=[],this.sep=function(){this.length>0&&"separator"!==this.array[array.length-1].fnc&&this.push({fnc:"separator"})},this.push=function(t){return void 0===t?null:(!0===Array.isArray(t)?Array.prototype.push.apply(this.array,t):"object"==typeof t&&this.array.push(t),t)},this.unshift=function(t){return void 0===t?null:(!0===Array.isArray(t)?Array.prototype.unshift.apply(this.array,t):"object"==typeof t&&this.array.unshift(t),t)},this.push(t)},t.menu=function(e,n){e=e||[];var i=$(this).removeClass("vis");if(!0===bool(n,!0)&&!1===i.is("#mainmenu")&&i.empty(),!1===bool(n,!1)&&i.is("#sidebar,#topbar")&&(e.unshift({id:"sbctrl",glyph:"glyphicon-th-list",aclass:"fbtn",fnc:function(){$lf()}}),$lf(0)),0===(e||[]).length)i.empty().addClass("hd");else{i.removeClass("hd");var o=!0===i.is("nav")?i:i.children("nav");1!==o.length&&(o=$("").tC("nv",i.is("#sidebar")).tC("ctxt",i.is("#topbar")).appendTo(i));var r,s=$$.ul().appendTo(o),a=function(t,e){var n=$(this).addClass("dropdown submenu");t.append($$.sc("caret dd")).addClass("dds dropdown-toggle").attr({"aria-expanded":"false"}),""!==(e.ico||"")&&t.prepend($$.sc("ico "+e.ico));var i=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(n);$.each(e.itm||[],(function(t,e){r.call(i,e)}))},c=function(t){$(this).tC("disabled","boolean"==typeof t.disabled?t.disabled:"string"==typeof t.disabled&&"subs"===t.disabled&&0===(t.itm||[]).length)};r=function(e){var n,i=$$.li({id:e.id}).attr(e.attr||{}).addClass(e.lclass).appendTo($(this)),o="string"==typeof e.fnc&&""!==e.fnc?e.fnc.split(":")[0]:"";""!==o&&"init"!==o?i.attr("role",o).appendIf($$.s(e.lbl),""!==ne(e.lbl)):(n=$$.a({class:"on",role:"button"}).addClass(e.aclass).appendTo(i).append($$.s(e.lbl)),c.call(n,e),(e.itm||[]).length>0&&a.call(i,n,e),n.click($nuf),"function"==typeof e.fnc?n.click(e.data||{},e.fnc):"init"===o&&n.click($.extend({},e.data||{},{fn:e.fnc.split(":")[1]}),t.init))},$.each(e,(function(e,n){var i,o=$$.li({id:n.id}).attr(n.attr||{}).addClass(n.lclass),a="string"==typeof n.fnc&&""!==n.fnc?n.fnc.split(":")[0]:"";if(""!==a&&"init"!==a)o.attr("role",a).appendIf($$.s(n.lbl),""!==ne(n.lbl));else{if(i=$$.a({class:"on",role:"button"}).addClass(n.aclass).appendTo(o),c.call(i,n),""!==(n.lbl||"")&&i.append($$.s(n.lbl)),""!==(n.ico||"")&&i.prepend($$.sc("ico "+n.ico)),""!==(n.glyph||"")&&i.prepend($$.sc("glyphicon "+n.glyph)),(n.itm||[]).length>0){o.addClass("dropdown"),i.append($$.sc("caret dd")).addClass("dds dropdown-toggle").attr({"aria-expanded":"false"});var l=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(o);$.each(n.itm||[],(function(t,e){r.call(l,e)}))}(n.sel||[]).length>0||(i.click($nuf),"function"==typeof n.fnc?i.click(n.data||{},n.fnc):"init"===a&&i.click($.extend({},n.data||{},{fn:n.fnc.split(":")[1]}),t.init))}o.appendTo(s)})),o.activatemenu()}},t.easytbl=(t,e)=>{e=e||{};let n=$$.tbl().addClass(e.class).css("border-collapse","collapse"),i=($$.tbody(n),!0===bool(e.frame,!1)?{padding:"5px",border:"1px solid #727272"}:{});if(!0===Array.isArray(e.header)){let t=$$.thead(n);$.each(e.header,((n,o)=>$$.th(t).css(e.cellcss||i).rwText(o)))}else if(!0===bool(e.header,!1)&&(t||[]).length>0){let o=$$.thead(n);$.each(Object.keys(t[0]),((t,n)=>$$.th(o).css(e.cellcss||i).rwText(n)))}return $.each(t||[],((t,o)=>{let r=$$.tr();$.each(o,((t,n)=>{n=n||"";let o=$$.td(r).css(e.cellcss||i);n instanceof jQuery?o.append(n):"string"==typeof n&&("<"===n.substring(0,1)?o.append(n):o.text(n))})),n.append(r)})),n},t.dlgtbl=(e,n,i)=>{i=i||{};let o=t.easytbl(e,i);t.dlg(o,$.extend({title:n},i))},t.dlg=function(t,n){n=n||{};let i=$("body > .modal").length>0,o=t=>typeof n[t],r=t=>"function"===o(t);if(!0===bool(n.exclusive,!0)&&!0===i)return void alert($t.dbldlg||"Es ist bereits ein Dialog geöffnet");let s=$$.dc("modal",$("body")),a=$$.dc("modal-dialog",s);!1===isNaN(n.zindex)?s.css("zIndex",n.zindex):!0===i&&s.css("zIndex",parseInt($("body > .modal:last").cssValue("zIndex"))+200),!1===isNaN(n.zindex_min)&&s.cssValue("zIndex")').appendTo(d)),""!==ne(n.title)&&(c=$$.dc("modal-header",d),$("

        ").text(n.title).appendTo(c));let p=$$.dc("modal-body",d),f=$$.dc("modal-footer",d);t instanceof jQuery==!0&&p.append(t);let m=function(t){t&&"function"==typeof t.stopPropagation&&t.stopPropagation(),a.removeClass("in"),!0===r("closing")&&n.closing.call(d),p.hide().emptyWithEditors(),s.remove(),!0===r("close")&&n.close.call(d)};if(d.find(":input[required]").length>0&&($$.dc("note_required",f).append($$.sc("ind_required","*")).append($$.s($t.t1||"Eingabe erforderlich")),$$.dc("note_invalid",f).append($$.s($t.t2||"Bitte überprüfen Sie Ihre Eingaben im Formular."))),!0===r("cancel")){$$.bbtn(n.cancelbutton||"Abbrechen","cancel").attr({type:"button",role:"cancel"}).appendTo(f).click((function(t){n.cancel.call(d,t);t.stopPropagation(),m()}))}if(!0===r("confirm")){let t=$$.bbtn(n.button||"OK","confirm").attr({type:!0===bool(n.form,!1)?"submit":"button",role:"confirm"}).appendTo(f);!0===h?(d.submit((function(t){try{n.confirm.call(d,t)}finally{t.preventDefault()}return!1})),d.on("modal_submit",(function(){n.confirm.call(d,e)}))):(t.click((function(t){n.confirm.call(d,t);t.stopPropagation()})),d.on("modal_submit",(function(){t.click()})))}else!0===h&&d.submit((function(t){return t.preventDefault(),!1}));return d.on("modal_close",(function(){m()})),l.click(m),!0===r("opening")&&n.opening.call(d),a.addClass("in"),ne(n.mode).indexOf("maxbody")>-1&&p.css("min-height",(u.height()-c.outerHeight()-f.outerHeight()).toString()+"px"),!0===r("open")&&n.open.call(d),{hd:c,bdy:p,ft:f,ct:u,dlg:a,c:d}},t.mform=function(e){let n=$$.dc("form-body"),i=Array.isArray(e)?e:e instanceof fields_definition?e.fields:[];return $.each(i||[],(function(e,i){let o=i.type||"";if("ignore"===o)return!0;let r=$$.dc("form-group",n),s=i.id||"dlg_"+(i.name||"")+("html"===i.type?"_"+(65536*(1+Math.random())||0).toString(16).substr(9):""),a=$$.lbl(i.label||i.name,{for:s}).appendTo($$.dc("form-itm",r)),c=$$.dc("form-itm",r),l=$$.i({id:s,name:i.name,placeholder:i.placeholder,type:i.type});switch(o){case"email":i.pattern=ne(i.pattern,"[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$");break;case"url":i.pattern=ne(i.pattern,"https?://.+");break;case"number":i.pattern=ne(i.pattern,"[-+]?[0-9]*[.,]?[0-9]*"),l.attr("type","text"),l.attr("inputmode","decimal"),l.attr("step",i.precision||"any"),l.attr("data-format","float");break;case"integer":case"int":i.pattern=ne(i.pattern,"[-+]?[0-9]*"),l.attr("type","number"),l.attr("data-format","integer");break;case"date":if(""!==ne(i.pattern,$t.datepattern)&&(i.pattern=ne(i.pattern,"("+$t.datepattern+")|([0-9]{4}.(0[1-9]|1[012]).(0[1-9]|1[0-9]|2[0-9]|3[01]))")),""!==ne(i.placeholder,$t.dateplaceholder)&&l.attr("placeholder",ne(i.placeholder,$t.dateplaceholder)),"string"==typeof i.value){var u=i.value.substr(0,10);i.value="date"!==l.prop("type")?fdt(u+"T00:00:00",ne(i.dateformat,$t.dateformat)):u}l.attr("data-format","date:"+ne(i.dateformat,$t.dateformat)+";yyyy-MM-dd");break;case"datetime":l.attr("type","datetime-local"),""!==ne(i.pattern,$t.datetimepattern)&&(i.pattern=ne(i.pattern,"("+$t.datetimepattern+")|([0-9]{4}-(0[1-9]|1[012])-(0[1-9]|1[0-9]|2[0-9]|3[01])\\s([0-5][0-9]):([0-5][0-9]))")),""!==ne(i.placeholder,$t.datetimeplaceholder)&&l.attr("placeholder",ne(i.placeholder,$t.datetimeplaceholder)),"string"==typeof i.value&&"T"===i.value.substr(10,1)&&(i.value="datetime"!==l.prop("type").substr(0,8)?fdt(i.value,ne(i.datetimeformat,$t.datetimeformat)):i.value),l.attr("data-format","datetime:"+ne(i.datetimeformat,$t.datetimeformat)+";yyyy-MM-dd HH:mm:ss");break;case"hidden":r.addClass("hd");break;case"html":case"text":l=$$.txt({id:s,name:i.name,placeholder:i.placeholder,type:i.type}),l.tC("tinymce","html"===i.type);break;case"bool":case"boolean":i.url=[{value:"true",label:($t||{}).true||"Yes"},{value:"false",label:($t||{}).false||"No"}],"boolean"==typeof i.value&&(i.value=i.value?"true":"false");case"select":l=$$.sel({id:s,name:i.name,type:i.type}),!1===bool(i.required,!1)&&$$.eOpt().appendTo(l);try{var d=function(t){!0===Array.isArray(t)&&$.each(t,(function(t,e){"string"==typeof e?$$.opt(e,e).appendTo(l):!0===Array.isArray(e)?$$.opt(e[0],e[1]).appendTo(l):"object"==typeof e&&$$.opt(e.value,e.label||e.text).appendTo(l)}))};!0===Array.isArray(i.url)?d(i.url):"function"==typeof i.url?i.url.call(l):"string"==typeof i.url&&t.postXT({url:i.url,success:d})}catch(t){$.noop()}break;default:""!==ne(i["max-length"])&&l.attr("max-length",i["max-length"])}""!==ne(i.pattern)&&l.attr("pattern",i.pattern),l.val(i.value).change(),l.change((function(){$(this)[0].setCustomValidity("")})),l.addClass("form-control").prop("required",bool(i.required,!1)).prop("readonly",bool(i.readonly,!1)).appendTo(c),!0===bool(i.required,!1)&&a.append($$.sc("ind_required","*")),"object"==typeof i.attr&&l.attr(i.attr),"object"==typeof i.prop&&l.prop(i.prop),"string"==typeof i.class&&l.addClass(i.class),"function"==typeof i.change&&(l.change(i.change),!0===bool(i.applychange,!1)&&void 0!==i.value&&l.change()),""!==(i.note||"")&&$$.dc("form-note",c).rwText(i.note),"function"==typeof i.complete&&i.complete.call(l)})),n},t.initMCE=function(t,e){t=$(t),e=e||{};try{let n={target:t[0],inline:!1,width:e.width||"100%",statusbar:!1,document_base_url:window.location.origin+"/",content_style:"ph:before {content: '«'; color: #BBB; font-style:italic; } ph:after {content: '»'; color: #BBB; font-style:italic; } ph { color: #AAA; font-style:italic; }",relative_urls:!1,remove_script_host:!1};!0===bool(e.hidemenu,!1)&&(n.menubar=!1,n.menu={}),!0===bool(e.hidetoolbar,!1)&&(n.toolbar=!1),$.extend(n,e||{}),tinymce.init(n)}catch(t){alert(t.message)}},t.dlgform=function(e,n){n=n||{};let i,o=$$.dc("frm").append(t.mform(e||[]).addClass("stacked"));n.addcontent instanceof jQuery&&o.append(n.addcontent),"function"==typeof n.submit?i=n.submit:"function"==typeof n.success&&(i=function(e){var i=$(this).ldng(1),o=$.extend({loginaccount:t.auth.account||""},i.serializeObject(bool(n.checkvalidity,!0),{typedvalues:bool(n.typedvalues,!1)}));""!==(n.url||"")?t.postXT({url:n.url,data:o,success:function(t){n.success.call(this,t),i.trigger("modal_close")},error:function(){alert($t.l17)},complete:function(){i.ldng(0)},timeout:6e4}):(n.success.call(this,o),i.trigger("modal_close"))});let r={form:!0,title:n.title||"",button:n.button||$t.submit,confirm:i,size:n.size||[500,600],open:function(){let e=$(this).find(".tinymce");e.length>0&&t.initMCE(e,n.tinymce||{})}};return t.dlg.call(this,o,r)},t.login.dlg=function(e){e=e||{};let n=[{name:"userinfo",label:$t.l1,type:"string",value:t.auth.login,change:t.login.uichange,required:!0},{name:"userlogin",type:"hidden",required:!0,value:t.auth.login},{name:"username",type:"string",label:$t.l4,required:!0,readonly:!0,placeholder:$t.l5,value:t.auth.fullname_rev},{name:"userpass",type:"password",label:$t.l3,required:!0,placeholder:$t.l3}];""===(t.auth.account||"")&&n.unshift({id:"dlg_loginaccount",name:"loginaccount",type:"string",required:!0,value:t.auth.account});let i=$$.dc("frm").append(t.mform(n).addClass("stacked")),o=t.dlg.call(this,i,{form:!0,title:$t.l0,button:$t.submit,confirm:function(n){var i=$(this).ldng(1),o=$.extend({loginaccount:t.auth.account||""},i.serializeObject());t.postXT({url:"/vt/login",data:o,success:function(n){""!==((n||{}).login||"")&&(i.trigger("modal_close"),t.auth=n,"object"==typeof e.ajo&&(e.ajo.islogin,$.ajax(e.ajo)))},error:function(){alert($t.l17)},complete:function(){i.ldng(0)},timeout:6e4})},size:[500,600]}),r=$$.dc("modal-content").css("height","auto").attr("novalidate","true").append($$.dc("modal-header").appendIf($("

        ").text(t.auth.accountname),""!==(t.auth.accountname||"")).append($("

        Vereinsmanager

        ")));o.dlg.prepend(r)},t.addNoEntryInfo=function(t){$(this).append($$.dc("noentryinfo").text(t||$t.t11))}}($ocms),Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector),Element.prototype.closest||(Element.prototype.closest=function(t){var e=this;do{if(Element.prototype.matches.call(e,t))return e;e=e.parentElement||e.parentNode}while(null!==e&&1===e.nodeType);return null}),function(t,e){var n,i;"object"==typeof window&&(window[t]=(n=function(t){var e=window,n=document.body,i=document.documentElement,o=Math.max(0,e.pageXOffset||i.scrollLeft||n.scrollLeft||0)-(i.clientLeft||0),r=Math.max(0,e.pageYOffset||i.scrollTop||n.scrollTop||0)-(i.clientTop||0);return{x:t?Math.max(0,t.pageX||t.clientX||0)-o:0,y:t?Math.max(0,t.pageY||t.clientY||0)-r:0}},(i=function(t,e){t&&t instanceof Element&&(this._container=t,this._options=e||{},this._clickItem=null,this._dragItem=null,this._showDragItem="boolean"!=typeof this._options.dragItem||!1!==this._options.dragItem,this._hovItem=null,this._sortLists=[],this._click={},this._dragging=!1,this._dragHandleClass=this._options.dragHandleClass||"",this._parentident=this._options.parentident||"",this._swapdone="function"==typeof this._options.swapdone?this._options.swapdone:null,this._onend="function"==typeof this._options.onend?this._options.onend:null,this._container.setAttribute("data-is-sortable",1),this._container.classList.add("sortable"),this._container.style.position="static",window.addEventListener("mousedown",this._onPress.bind(this),!0),window.addEventListener("touchstart",this._onPress.bind(this),!0),window.addEventListener("mouseup",this._onRelease.bind(this),!0),window.addEventListener("touchend",this._onRelease.bind(this),!0),window.addEventListener("mousemove",this._onMove.bind(this),!0),window.addEventListener("touchmove",this._onMove.bind(this),!0))}).prototype={constructor:i,toArray:function(t){t=t||"id";for(var e=[],n="",i=0;ii.left&&ei.top&&n0?a.mousedown(c).addClass("dctrl"):s.mousedown(c).addClass("dctrl"),t(this)}}(jQuery),$(document).ready((function(){$("html").click((function(t){$nuf()})),$("#listframe").click((function(t){t.stopPropagation(),$nuf()})),$("#mainmenu").ocmsmenu($ocms.ocmsmenu),$("#mainmenu").activatemenu()})),$.extend($t,{m_inv:"Rechnungen",m_req:"Aufträge",m_rep:"Berichte",m_todo:"ToDos",m_bcd:"BankBuchungen",m_admin:"Administration",rsp:"Passwort ändern",pnm:"Die Passwörter stimmen nicht überein",cps:"Das neue Passwort wurde gespeichert.",pwr:"Bitte wählen Sie ein starkes Passwort (min 8 Zeichen, davon jeweils min 2 Zahlen, kleine und große Buchstaben, Sonderzeichen sind optional).",smsc:"Sie beötigen für diese Funktion einen SMS-Code.\nSoll dieser nun versandt werden?",wdc:"Doppelt klicken, um die Box zu aktualisieren.",wdg:{}}),$t.rspf={sms:"Der SMS-Code konnte nicht bestätigt werden",valid:"Das alte Passwort ist nicht korrekt",requirements:"Das Passwort entspricht nicht den Anforderungen.\n"+$t.pwr},$fd={rsp:new fields_definition("","",[{name:"opw",label:"aktuelles Passwort",type:"password",required:!0,attr:{"auto-complete":"current-password"}},{name:"npw",label:"neues Passwort",type:"password",required:!0,pattern:"(.{6,})",attr:{"auto-complete":"new-password"}},{name:"npwc",label:"neues Passwort (Bestätigung)",type:"password",required:!0,attr:{"auto-complete":"new-password"},note:$t.pwr},{name:"code",label:"SMS-Code",type:"string",required:!0,attr:{"auto-complete":"one-time-code"}}])},$ocms.init=function(t){var e="string"==typeof t?t:(t.data||{}).fn||"";""!==e&&("home"===e?($cfr(),$lfr(),$("#topbar").ocmsmenu([],!0),$("#activemodule").text($t.ov),$fis.ov()):($cfr(),$lfr(),$("#topbar").ocmsmenu([]),$ocms.postXT({url:$ocms.url(e+"/auth"),success:function(t){void 0===$ocms[e]&&($ocms[e]={}),$ocms[e].auth=t,t.manage>0&&$ocms.getScript({module:e,script:["/web/fis",e,$ocms.auth.locale||"de","js"].join("."),css:["/web/fis",e,"css"].join("."),condition:"function"!=typeof $ocms[e].init2},(function(){$ocms[e].init2()}))},error:function(){$("#contentframe").empty()}})))};var $fis={auth:{},db:function(){$("#mainmenu_activemodule").text($t.ov);let t=$(this).empty(),e=$$.d({id:"dashboard_frame"}).appendTo(t);$ocms.postXT({url:$ocms.url("wdg/my"),success:function(t){$.each(t,(function(t,n){var i=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$ocms.wdg.call(i,{wdg:n})}))},loading:e})},ValidateEmail:function(t){return!!/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(t)},cf:t=>{let e=$("#contentframe");return!0===bool(t,!1)&&e.empty().rC("hd"),e},lf:t=>{let e=$("#listframe");return!0===bool(t,!1)&&e.empty().aC("hd").rC("fix"),e},frm_edit:function(t){let e=$fis.cf(!1),n=e.children(".cfrm"),i=e.children(".edit_frm");return n.length<1?n=$$.dc("cfrm hd").prependTo(e):!0===bool(t,!1)&&n.empty(),i.length<1&&(i=$$.dc("edit_frm").insertAfter(n)),i.empty()},frm_list:function(t,e){let n=$fis.cf(!1),i=n.children(".cfrm"),o=n.children(".list_frm");return i.length<1?i=$$.dc("cfrm hd").prependTo(n):!0===bool(t,!1)&&i.empty(),!0===bool(e,!1)&&n.children(".edit_frm").remove(),o.length<1&&(o=$$.dc("list_frm").appendTo(n)),o.empty()},lfm:()=>{let t=$fis.lf(!1),e=t.children(".lfrm");return e.length<1&&(e=$$.dc("lfrm").prependTo(t)),e},getAuth:(t,e)=>new Promise(((n,i)=>{$fis.auth[t]&&!1===bool(e,!1)?n($fis.auth[t]||-1):$ocms.postXT({url:$ocms.url("auth"),data:{module:t},success:e=>{$fis.auth[t]=e.auth||-1,n($fis.auth[t]||-1)},error:()=>{i()}})})),prepAuth:t=>new Promise(((e,n)=>{$ocms.postXT({url:$ocms.url("auth"),data:{module:t,array:1},success:t=>{$.extend($fis.auth,t||{})},complete:()=>{e()}})})),isAuth:(t,e)=>($fis.auth[t]||-1)>=(e||1),resetPass:function(t,e){confirm($t.smsc)&&($ocms.postXT({url:$ocms.url("account/sms"),data:{fn:"pwc"}}),$ocms.dlgform($fd.rsp.clone(),{title:$t.rsp||"",submit:function(t){var e=$(this).ldng(1),n=$.extend({loginaccount:$ocms.auth.account||""},e.serializeObject(!0,{typedvalues:!0}));(n.npw||"")!==(n.npwc||"")?e.find('input[name="npw"]:first')[0].setCustomValidity($t.pnm):$ocms.postXT({url:$ocms.url("account/changepassword"),data:n,success:function(t){alert($t.cps),e.trigger("modal_close")},error:function(t){alert($t.rspf[t.getResponseHeader("x-ocms-std")])},complete:function(){e.ldng(0)},timeout:6e4})}}))},wdg:function(t){let e=$(this).empty();$ocms.postXT({url:$ocms.url("wdg/one"),data:{short_name:t.wdg},timeout:9e4,success:function(n,i,o){let r=t.wdg,s=n[r];if(!s)return void e.ldng(0);let a=$.inArrayRegEx("dblwidth",s.rendering_options)>-1,c=$.inArrayRegEx("tiny",s.rendering_options)>-1;e.toggleClass("dbl",a&&!c).toggleClass("tny",c);$$.dc("wdg_hd",e,{title:ne(s.description,$t.wdc)}).toggleClass("dbl",a).text(ne(s.name,t.wdg)).dblclick((function(t){t.stopPropagation(),$fis.wdg.call(e,{wdg:r})}));let l=$$.dc("wdg_cnt",e).toggleClass("dbl",a).hide(),u=$.inArrayRegEx("bgcolor",s.rendering_options);switch(u>-1&&l.css("backgroundColor",s.rendering_options[u].toString().right(":")),s.type){case"table":var d=$$.tblset({},l),h=$$.tr().appendTo(d.hd),p=$t.wdg[r.indexOf("wdg_ev_")>=0?"wdg_ev_":r]||{};$.each(s.columns,(function(t,e){var n=p[e]?p[e].label:e;$$.th().text(n).appendTo(h)})),$.each(s.data,(function(t,e){var n=$$.tr().appendTo(d.bdy);$.each(s.columns,(function(t,i){var o=$$.td().appendTo(n);e[i]instanceof Date||!0===$ocms.isJSONDateString(e[i])?o.text(fdt(e[i],$t.dateformat)):o.rwText(e[i])}))})),$.inArray("firstrow_bold",s.rendering_options)>-1&&h.nextAll("tr:first").css("font-weight","bold");break;case"ind":$$.dc("ind",l).addClass("sts_"+(s.data.status||"")).append([$$.dc("ind").text(s.data.value),$$.lbl(s.data.label)]);break;case"image_url":l.css("background","url('"+s.url+"') no-repeat center center transparent");break;case"image_base64":l.css("background","url('data:image/png;base64,"+s.image+"') no-repeat center center transparent");break;case"html":if(l.html(s.html),$.inArray("reload_10min",s.rendering_options)>-1){var f=l.find("iframe");setTimeout((function(){f.attr("src",(function(t,e){return e}))}),6e5)}}$.inArray("reload_30min",s.rendering_options)>-1&&"html"!==s.type&&setTimeout((function(){$fis.wdg.call(e,{wdg:r})}),18e5),l.slideDown(150)},error:function(t){e.slideUp(150),$fis.failure.call(this,t)},complete:function(){e.ldng(0)}})},ov:function(){$fis.lf(!0);let t=$("#contentframe").empty(),e=$$.d({id:"dashboard_frame"}).appendTo(t);$ocms.postXT({url:$ocms.url("wdg/my"),success:function(t){$.each(t,(function(t,n){var i=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$fis.wdg.call(i,{wdg:n})}))},loading:e})}};$fis.notifications={connection:null,init:function(){"undefined"!=typeof signalR&&null===this.connection&&$ocms.auth.useraccount_id&&(this.ensureFrame(),this.connection=(new signalR.HubConnectionBuilder).withUrl("/notifications").withAutomaticReconnect().build(),this.connection.on("notification",(t=>{this.push(t)})),this.connection.onclose((()=>{console.warn("Notification connection closed; retrying in 5s."),this.connection=null,setTimeout((()=>this.init()),5e3)})),this.start())},start:function(){this.connection.start().catch((t=>{console.warn("Notification connection failed to start; retrying in 5s.",t),this.connection=null,setTimeout((()=>this.init()),5e3)}))},ensureFrame:function(){$("#notification_frame").length<1&&$("
        ",{id:"notification_frame"}).appendTo($("footer:first").length?"footer:first":"body")},push:function(t){this.ensureFrame(),t=t||{};let e=$("
        ",{class:"notification_item"}).addClass((t.severity||"info").toLowerCase()).append($("
        '),n=t.find(".form-body"),i=null;t.find("form").submit((function(e){e.preventDefault();var r=$(this).serializeObject(!0),o=null===i,a=o?"spwc":"spw";return $ocms.postXT.call(this,{url:$ocms.url(a),data:r,complete:function(){o?(n.append('
        Ihnen wurde ein Code per SMS zugesandt.
        Bitte tragen Sie den hier ein:
        '),i=$('
        ').appendTo(n)):(alert($t.l13),t.remove())},error:()=>{}}),!1})),t.find(".modal-close").click((function(){t.remove()}));var r=[];$.each($t.l7a.split("\n"),((e,t)=>{Array.prototype.push.apply(r,[$("
        "),$("").text(t)])})),t.find(".modal-note").append($('').text($t.alert)).append(r),t.appendTo("body"),setTimeout((function(){$(".modal").find('input[name="lastname"]').focus()}),600)}},$(document).ready((function(){$("#loginform").submit($ocms.login.send)})); \ No newline at end of file +var $t={lng:"de-DE",dn:["So","Mo","Di","Mi","Do","Fr","Sa"],mn:["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],ma:["Jan","Feb","Mrz","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],datepattern:"(0[1-9]|1[0-9]|2[0-9]|3[01]).(0[1-9]|1[012]).[0-9]{4}",datetimepattern:"(0[1-9]|1[0-9]|2[0-9]|3[01]).(0[1-9]|1[012]).[0-9]{4}\\s([0-5][0-9]):([0-5][0-9])",dateplaceholder:"dd.MM.yyyy",datetimeplaceholder:"dd.MM.yyyy HH:mm",dateformat:"dd.MM.yyyy",datetimeformat:"dd.MM.yyyy HH:mm",f1:"Der Server hat einen Fehler gemeldet: \n",f2:"Bitte versuchen Sie es erneut.",m0:"Diese Internet-Seite benötigt einen html5-kompatiblen Browser.",m0b:"Unterstützt werden bspw: Internet Explorer ab Version 10, Firefox ab Version 31, Chrome ab Version 31, Safari ab Version 7, Opera ab Version 27",m1:"Dieser Datensatz ist momentan von jemand anderem zur Bearbeitung gesperrt.",m2:"Diese Funktion ist zur Zeit nicht verfügbar",t1:"Eingabe erforderlich.",t2:"Eingabe ist nicht erforderlich.",true:"Ja",false:"Nein",alert:"Hinweis",confirm:"Bestätigen",open:"Öffnen","not implemented":"Diese Funktion in zur Zeit noch nicht verfügbar.",l0:"Anmeldung",l1:"Email / Anmeldename",l2:"Email-Adresse / Anmeldename",l3:"Passwort",l4:"Benutzer",l5:"Wird vom System ermittelt...",l6:"Anmelden",l7:"Passwort vergessen?",l7a:'Die "Passwort vergessen"-Funktion läuft in zwei Schritten ab:\n \nIm ersten Schritt wird eine SMS mit einem Code an die hinterlegte Mobilfunk-Nummer versandt.\nIm zweiten Schritt geben Sie bitte diesen Code in das Formular ein und übermitteln es erneut.\n \nIn beiden Schritten wird aus Sicherheitsgründen kein Fehler angezeigt und auch dann ein erfolgreicher Versand bestätigt, wenn die Kombination aus Email-Adresse und Nachname nicht gefunden wurde und/oder der code falsch ist.',l8:"Keinen Account?",l9:"Anmeldenamen der Email-Adresse wurde nicht erkannt.",l10:"Nachname",l11:"Email-Adresse",l12:"Passwort zusenden",l13:"Das Passwort wurde erfolgreich verschickt",l14:"Das Passwort konnte nicht verschickt werden",l15:"Sie sind nicht berechtigt, diese Funktion auszuführen.",l16:"Sie müssen zunächst einen Account angeben.",l17:"Die Kombination aus Anmeldenamen und Passwort konnte nicht bestätigt werden.",l18:"Es gibt ein Problem mit dem Formular.\nEs kann momentan nicht verarbeitet und versendet werden.",name:"Name",submit:"Senden",cancel:"Abbrechen",noop:"Diese Funktion is noch nicht verfügar."};const isIE=/MSIE\/|Trident/gi.test(window.navigator.userAgent)||void 0!==window.document.documentMode,isfileapi=!!(window.File&&window.FileReader&&window.FileList&&window.Blob);var $ocms={auth:{},no:function(e){e.stopPropagation()},vmin:function(e){var t=$(window).width*(e||1),n=$(window).height*(e||1);return t($ocms.baseurl+"/"+(e||"")).replace(/\/\//,"/"),cexi:null};function deepCopy(e){var t,n,i;if("object"!=typeof e||null===e)return e;for(i in t=Array.isArray(e)?[]:{},e)n=e[i],t[i]=deepCopy(n);return t}function fields_definition(e,t,n){this.label_sng=!0===Array.isArray(e)?"":e||"",this.label_pl=!0===Array.isArray(e)?"":t||"",this.fields=!0===Array.isArray(e)?e:n||[],this.itm=function(e){for(var t=0;t0)for(var n=0;ne||"")).filter(((e,t)=>""!==e)).join(t)}function parseDt(e,t,n){e=(e||"").substr(0,t.length);var i=t,r=e.length>0&&t.split(";").some((function(t){for(var n,r=/[^yMdhms0-9]/gi,o=!0;null!==(n=r.exec(t));)o=o&&t.substr(n.index,1)===e.substr(n.index,1);var a=e.length===t.length&&o;return!0===a&&(i=t),a}));if(!0===r){for(var o,a=[0,0,0,0,0,0,0],s=/(mm{1,2}|dd{1,2}|MM{1,2}|(yy){2,4}|ss{1,2}|hh{1,2})(?!\w)/g;null!==(o=s.exec(i));)a["yMdhms".indexOf(o[0].substr(0,1))]=parseInt(("yy"===o[0]?"20":"")+e.substr(o.index,o[0].length))-("M"===o[0].substr(0,1)?1:0);var l=new(Function.prototype.bind.apply(Date,[null].concat(a)));return"string"==typeof n?fdt(l,n):l}return!1}function bool(e,t){return"boolean"==typeof e?e:"boolean"==typeof t&&t}function booln(e,t){return"boolean"==typeof e?e:"number"==typeof e?1===e:"boolean"==typeof t&&t}Date.prototype.isValid=function(){return!isNaN(this)},Date.prototype.format=function(e){return fdt(this,e)},Date.prototype.addDays=function(e){return this.setDate(this.getDate()+e),this},Date.prototype.isBetween=function(e,t){return this>e&&this section");$(window).scroll((function(t){let n=$(window).scrollTop(),i=$("body");i.toggleClass("unfocus",n>vh()-1.2*e),i.toggleClass("btb",n>.5*vh()-e)}))},$ocms.cf_reset=function(){return $("#contentframe").empty()},function(e){e.fn.scrollTo=function(t){if(e(this).length>0){var n=e(this).offset().top||0;n>0&&e("html, body").animate({scrollTop:n-hh()},2e3)}},e.fn.ldng=function(t){var n=!0;return"boolean"==typeof t?n=t:"number"==typeof t&&(n=t>0),e(this).toggleClass("loading",n)},"function"!=typeof e.noop&&(e.noop=function(){}),e.fn.hasAttr=function(t){var n=e(this).attr(t);return void 0!==n&&!1!==n},e.fn.parseCssPx=function(t){try{return parseFloat(e(this).css(t).replace("px","")||0)}catch(e){return 0}},e.max=function(e,t){return isNaN(e)&&isNaN(t)?null:isNaN(e)&&!isNaN(t)?t:!isNaN(t)&&isNaN(t)||e>=t?e:t},e.min=function(e,t){return isNaN(e)&&isNaN(t)?null:isNaN(e)&&!isNaN(t)?t:!isNaN(t)&&isNaN(t)||e<=t?e:t},e.lim=function(e,t){return isNaN(e)?null:isNaN(t)?e:t<=e?t:e},e.fn.enterKey=function(t){return this.each((function(){e(this).keypress((function(e){"13"===(e.keyCode?e.keyCode:e.which).toString()&&t.call(this,e)}))}))}}(jQuery),$ocms.defaultTimeout=3e4,$ocms.AjaxEX=function(e){var t=this;t.responseText=t.responseText||"";var n=t.getResponseHeader("x-ocms-code")||"";t.internalCode=""!==n&&!1===isNaN(n)?parseInt(n):-1,t.isInternal=t.internalCode>-1,t.internalText=decodeURIComponent((t.getResponseHeader("x-ocms-desc")||"").replace(/\+/g,"%20")||"");var i=t.internalText||e,r=t.internalCode||t.status;t.logtext=i+" ("+r+")"},$ocms.postXTS=function(e){$ocms.postXT.call(this,$.extend(e,{sync:!0}))},$ocms.postXT=function(e){if((e=e||{}).trycount=e.trycount||0,""!==(e.url||"")){e.url=-1!==e.url.indexOf("&yy=")?e.url:e.url.indexOf("?")>-1?e.url+"&yy="+(new Date).getTime():e.url+"?yy="+(new Date).getTime();var t=e.context||this;switch(e.context=t,e.retryLimit=e.retryLimit||0,e.timeout=e.timeout||$ocms.defaultTimeout,e.timeout<100&&(e.timeout=1e3*e.timeout),e.data=e.data||{},e.contentType=e.contentType||"multipart/form-data; charset=UTF-8",e.islogin="boolean"==typeof e.islogin&&e.islogin,e.contentType){case"":case"json":e.contentType="application/json; charset=utf-8";break;case"form":e.contentType="application/x-www-form-urlencoded; charset=UTF-8";break;case"multi":e.contentType="multipart/form-data";break;case"text":e.contentType="text/plain; charset=UTF-8"}if(e.form instanceof jQuery?(e.data=e.form.serializeObject(),e.contentType="form-data"):e.lzw instanceof jQuery&&(e.data.lzw=$.ccLZW(e.lzw.serializeAnything(!0)).join(",")),"multipart/form-data"!==e.contentType.substr(0,19)&&"form-data"!==e.contentType.substr(0,9)||e.data instanceof FormData!=!1)e.data instanceof FormData&&(e.contentType=!1,e.processData=!1);else{e.contentType=!1;var n=new FormData;$.each(e.files||[],(function(e,t){n.append("upload_file",t)})),$.each(e.data||{},(function(e,t){n.append(e,t)})),e.data=n,e.processData=!1}var i={type:e.method||"post",url:e.url,data:e.data,processData:"boolean"!=typeof e.processData||e.processData,contentType:e.contentType,cache:e.cache||!1,timeout:e.timeout,beforeSend:function(n){$(e.loading).ldng(),$("body").addClass("ldng"),"function"==typeof e.beforesend&&e.beforesend.apply(t,[n])},success:function(n,i,r){"false"===n||"not authorized"===n?("function"==typeof e.error&&e.error.apply(t,[r,i,n]),"function"==typeof $.status&&$.status(i+" - "+n)):"function"==typeof e.success&&e.success.apply(t,[n,i,r])},error:function(n,i,r){if($ocms.AjaxEX.call(n,i),-1===e.url.indexOf("doc.ashx")||-1!==e.url.indexOf("ftest")){if(401===n.status&&111===n.internalCode&&!1===e.islogin&&"function"==typeof $ocms.login.dlg)$ocms.login.dlg({ajo:e});else if("timeout"===i||302===n.status)return e.tryCount++,e.tryCount<=e.retryLimit?void $ocms.postXT(e):void 0;"function"==typeof e.error?e.error.apply(t,[n,i,r]):"function"==typeof $ocms.failure?$ocms.failure.apply(t,[n]):"function"==typeof $.status&&$.status("Server error: "+i+" - "+r)}},dataType:e.datatype||"json",complete:function(n,i){"function"==typeof e.complete&&e.complete.apply(t,[n,i]),$(e.loading).ldng(0),$("body").removeClass("ldng");let r=$("body > .timer");if(r.length>0){let e=new Date(n.getResponseHeader("ocms_cec")||""),t=new Date(n.getResponseHeader("ocms_cex")||"");if(e.isValid()&&t.isValid()){let n=new Date,i=Math.abs(t-e);n.setMilliseconds(n.getMilliseconds()+i),r.data({cex:n,ctt:i}),$ocms.cex_timer()}}},context:t,async:!0};return"boolean"==typeof e.sync&&(i.async=!1===e.sync),!0==("boolean"==typeof e.contentType&&!1===e.contentType)&&(i.contentType=!1),$.ajax(i)}},$ocms.cex_timer=function(){$ocms.cexi||($ocms.cexi=setInterval($ocms.cex_timer,15e3));let e=$("body > .timer"),t=e.data("cex"),n=e.data("ctt"),i=new Date;if(t instanceof Date&&t.isValid()&&"number"==typeof n&&n>0&&t>i){let r=Math.abs(i-t)/n*100;e.css("width",r.toString()+"%"),r<98&&(!$ocms.cex_lp||Math.abs(i-$ocms.cex_lp)>6e5)&&$ocms.postXT({url:$ocms.url("ping"),success:()=>{$ocms.cex_lp=i},timeout:5e3,error:()=>{}})}},$ocms.vbl_send=function(e){var t=e.data||{};if(""!==(t.url||"")){var n=$("#contentframe form:first"),i={url:t.url,data:new FormData,success:function(e){"function"==typeof t.success?t.success(e):"string"==typeof t.success&&alert(t.success)},error:function(e,n,i){"function"==typeof t.error?t.error(i):"string"==typeof t.error&&alert(t.error)},complete:function(){n.ldng(0)}},r=!0;n.find("input").each((function(){var e=$(this),t=e.nza("name"),n=e.val(),o=$(this).prop("required")||!1;if(""!==t){var a=""!==n||!1===o;r=r&&a,!0===a?(i.data.append(t,n),e[0].setCustomValidity("")):""!==$(this).nza("ocms-nvnote")&&e[0].setCustomValidity($(this).nza("ocms-nvnote"))}})),!0===r&&(n.ldng(1),$ocms.postXT.call(this,i))}},function(e){e.fn.nza=function(t,n){var i=e(this).attr(t);return void 0!==i&&!1!==i?i:n||""},e.fn.serializeObject=function(t,n){var i=/\r?\n/g,r=/^(?:submit|button|image|reset|file)$/i,o=/^(?:input|select|textarea|keygen)/i,a=/^(?:checkbox|radio)$/i,s=bool((n=n||{}).typedvalues,!1),l={},c=e(this),u=c.find(':input:not([nosend],[type="file"])').addBack(":input"),d=!0;return e.each(u.not(".tinymce").get(),(function(n,c){var u=e(this),f=this,m=(this.type||"").toLowerCase(),p=u.prop("required")||!1;if(!0===(f.name&&!u.is(":disabled")&&o.test(f.nodeName)&&!r.test(m))){var h=u.val(),g=f.name,y=u.nza("data-format").split(":"),v=u.nza("pattern")||".*";if(!0===a.test(m)&&(h=f.checked?""!==h?h:"true":""),"date"===y[0].substr(0,4)&&y.length>1)"boolean"==typeof(h=parseDt(h,y.slice(1).join(":")))&&(h=null),null===h&&"date"===u.prop("type").substr(0,4)&&!1===isNaN(new Date(u.val()))&&(h=new Date(u.val())),h instanceof Date==!0&&"function"==typeof h.getMonth?!1===s&&(h=fdt(h,"date"===y[0]?"dts":"iso")):h=null;else if(("number"===m||"float"===y[0]||"integer"===y[0])&&!0===s){let e,t=String(h).replace(",",".");e="integer"===y[0]?parseInt(t):parseFloat(t),h=isNaN(e)?h:e}if(!0===p&&(""===h||null==h||"string"==typeof h&&null===h.match(v))?(!0===bool(t,!1)&&f.setCustomValidity(u.nza("ocms-nvnote",$ocms.t.inv||"Invalid field")),h=null):!0===bool(t,!1)&&f.setCustomValidity(""),null!=h&&"string"==typeof h){let e=l[g];null!=e?Array.isArray(e)?e.push(h.replace(i,"\r\n")):l[g]=[e,h.replace(i,"\r\n")]:l[g]=h.replace(i,"\r\n")}else if(null!=h){let e=l[g];null!=e?Array.isArray(e)?e.push(h):l[g]=[e,h]:l[g]=h}else d=!1}})),u.filter(".tinymce").each((function(t,n){var i=e(this),r=((this.type||"").toLowerCase(),i.prop("required")||!1);try{var o=tinymce.get(e(n).attr("id"));if(o){var a=e(n).attr("name"),s=o.getContent();!1===r||""!==(s||"")?l[a]=s:d=!1}}catch(t){e.noop()}})),c.toggleClass("invalid",!d),d?l:null},e.fn.sendForm=function(t,n,i){var r=e(this);i=i||{};var o={url:t,success:function(e){if(i.response=e,"function"==typeof n)n(e);r.closest("div.modal").remove()},error:function(e,t,n){"function"==typeof i.error?i.error.call(this,e):$ocms.failure.call(this,e)},complete:function(){r.ldng(0),"function"==typeof i.complete&&i.complete.call(this,jqXHR)}},a=r.find('input[type="file"]');o.data=new FormData,a.length>0&&e.each(a[0].files,(function(e,t){o.data.append(e,t),o.data.append("file_lastmodified",$ocms.isodt(t.lastModifiedDate))}));var s=r.serializeObject();e.each(s||{},(function(e,t){o.data.append(e,t)})),r.ldng(),$ocms.postXT.call(this,o)},e.fn.checkValidity=function(){var t=e(this),n=!0;return t.each((function(e,t){n=n&&t.checkValidity()})),n},e.fn.wrap=function(t,n){var i=e(this),r=$$.dc(t).attr(n||{}).insertAfter(i);return i.append(r),r}}(jQuery),$ocms.logout=function(){$ocms.postXT({url:$ocms.url("logout"),complete:function(){window.location.reload()}})},$ocms.login={send:function(e){e.preventDefault();var t=$(this);if(!0===t.find("#dbtn-confirm").hasClass("disabled"))return!1;var n=t.serializeObject();return n.loginaccount=ne(n.loginaccount,$ocms.auth.account||$ocms.auth.requestedaccount||""),n.loginaccount=ne(n.loginaccount,$ocms.auth.account||$ocms.auth.requestedaccount||""),""===ne(n.loginaccount)&&!0===bool($ocms.auth.accountrequired,!0)?(alert($t.l16),!1):($ocms.postXT({url:$ocms.url("login"),data:n,success:function(){window.location.reload()}}),!1)},uichange:function(){let e=$(this),t=e.closest("form"),n=bool($ocms.auth.accountrequired,!0),i=ne(t.find('[name="loginaccount"]').val(),$ocms.auth.account||$ocms.auth.requestedaccount||"");if(""!==i||!1===n){var r=t.find('[name="userlogin"]').empty().val(""),o=t.find('[name="username"]').empty().val(""),a=$("#dlg_userlogin_sel").empty().val(""),s=e.val()||"";if(!1===e.checkValidity()&&""===s)return;var l=e.closest("table").ldng();$ocms.postXT.call(this,{url:$ocms.url("auth"),data:{userinfo:s,account:i||""},success:function(e,t,n){if(1===e.length){var i=e[0];r.val(i.login).change().attr("required","").removeAttr("nosend"),o.val(i.name).change().attr("required","").show(),a.removeAttr("required").attr("nosend","").hide()}else e.length>0?(o.hide().removeAttr("required"),r.removeAttr("required").attr("nosend",""),0===a.length&&(a=$("").attr({name:"userlogin",size:e.length,id:"dlg_userlogin_sel",class:"form-control",required:""}).css({width:"100%","max-width":"100%",padding:"2px"}).insertAfter(o)),$.each(e,(function(e,t){var n=$("").attr({value:t.login,style:"padding-top: 2px; padding-bottom: 5px;","border-bottom":"1px solid #EEE;"}).text(t.name).appendTo(a);e%2==0&&n.css({"background-color":"#F9F9F9"})})),a.attr("required","").removeAttr("nosend")):(a.hide().attr("nosend",""),o.attr("required","").show(),r.attr("required","").removeAttr("nosend"),alert($t.l9))},error:function(e){$ocms.failure.call(this,e)},complete:function(){l.ldng(0)}})}else alert($t.l18)},sendpassword:function(e){var t=$(''),n=t.find(".form-body"),i=null;t.find("form").submit((function(e){e.preventDefault();var r=$(this).serializeObject(!0),o=null===i,a=o?"spwc":"spw";return $ocms.postXT.call(this,{url:$ocms.url(a),data:r,complete:function(){o?(n.append('
        Ihnen wurde ein Code per SMS zugesandt.
        Bitte tragen Sie den hier ein:
        '),i=$('
        ').appendTo(n)):(alert($t.l13),t.remove())},error:()=>{}}),!1})),t.find(".modal-close").click((function(){t.remove()}));var r=[];$.each($t.l7a.split("\n"),((e,t)=>{Array.prototype.push.apply(r,[$("
        "),$("").text(t)])})),t.find(".modal-note").append($('').text($t.alert)).append(r),t.appendTo("body"),setTimeout((function(){$(".modal").find('input[name="lastname"]').focus()}),600)}},$(document).ready((function(){$("#loginform").submit($ocms.login.send)})); \ No newline at end of file diff --git a/Fuchs_DataService/7z.dll b/Fuchs_DataService/7z.dll deleted file mode 100644 index b32d7bf..0000000 Binary files a/Fuchs_DataService/7z.dll and /dev/null differ diff --git a/Fuchs_DataService/FdsMain.cs b/Fuchs_DataService/FdsMain.cs deleted file mode 100644 index dc47e29..0000000 --- a/Fuchs_DataService/FdsMain.cs +++ /dev/null @@ -1,90 +0,0 @@ -using fds.Logging; -using Microsoft.Extensions.Logging; -using Topshelf; - -namespace fds; - -public class FdsService : ServiceControl -{ - private readonly PeriodicHostedService _hostedService; - private readonly CancellationTokenSource _cts = new(); - - public FdsService() - { - var loggerFactory = LoggerFactory.Create(b => - b.SetMinimumLevel(LogLevel.Debug).AddFdsLogging()); - - var mfr = new FdsMfr( - loggerFactory.CreateLogger(), - loggerFactory); - var interval = TimeSpan.FromMinutes(FdsConfig.ExecutionFrequency_Minutes); - var jobs = new[] - { - new PeriodicJobDefinition("MfrSync", interval, async ct => - { - bool debug = FdsConfig.DebugDetails; - await mfr.UpdateIfNecessary_async(debug, ct); - await mfr.UpdateRequested_async(debug, ct); - await mfr.GetInvoiceFiles_async(debug, ct); - }) - }; - - var logger = loggerFactory.CreateLogger(); - _hostedService = new PeriodicHostedService(jobs, logger); - } - - public bool Start(HostControl hostControl) - { - _ = _hostedService.StartAsync(_cts.Token); - return true; - } - - public bool Stop(HostControl hostControl) - { - _cts.Cancel(); - _hostedService.StopAsync(CancellationToken.None).GetAwaiter().GetResult(); - return true; - } -} - -public static class FdsMainModule -{ - public static void Main(string[] args) - { - FdsConfig.Initialize(); - - string machineName = Environment.MachineName.ToLower(); - if (!new[] { "digital-pc", "digital-dpc" }.Contains(machineName)) - { - HostFactory.Run(x => - { - x.Service(s => - { - s.ConstructUsing(name => new FdsService()); - s.WhenStarted((tc, host) => tc.Start(host)); - s.WhenStopped((tc, host) => tc.Stop(host)); - s.BeforeStoppingService(ctx => - { - if (FdsConfig.DebugDetails) Task.Run(() => FdsDebug.DebugToFile("fds__data_service - beforestop", filename: "DebugDetail.txt")); - }); - s.WhenPaused((tc, host) => tc.Stop(host)); - s.WhenContinued((tc, host) => tc.Start(host)); - }); - x.EnablePauseAndContinue(); - x.StartAutomatically(); - x.RunAsLocalSystem(); - x.SetDescription("MFR Data Sync"); - x.SetDisplayName("MFR Data Sync"); - x.SetServiceName("MFR Data Sync"); - }); - } - else - { - var svc = new FdsService(); - svc.Start(null!); - Console.WriteLine("Running locally — press any key to stop."); - Console.ReadKey(); - svc.Stop(null!); - } - } -} diff --git a/Fuchs_DataService/FdsMfr.cs b/Fuchs_DataService/FdsMfr.cs index 0b47cfd..1e15426 100644 --- a/Fuchs_DataService/FdsMfr.cs +++ b/Fuchs_DataService/FdsMfr.cs @@ -319,16 +319,13 @@ public class FdsMfr : IFdsMfr var archiveFile = new FileInfo(Path.GetTempPath() + $"DatevUpload_AR {startdate:yyyyMM}_{admin["mode"]}.zip"); try { - using var archive = new Archive(archiveFile, type: SevenZip.OutArchiveFormat.Zip, - logger: _loggerFactory.CreateLogger()); - if (archive.CompressToStream(fls, targetstream: stream!)) - { - stream!.Position = 0; - return archiveFile; - } - _logger.LogError( - "getDatevZip: CompressToStream returned false — archiveFile={ArchiveFile} fileCount={FileCount}", - archiveFile.FullName, fls.Count); + // Native .NET zip via the OCORE helper (System.IO.Compression) — no external + // 7-Zip dependency. The DATEV export is a plain, unencrypted zip, so ZipArchive + // covers it fully. filesToZipArchive skips null/empty entries. + var zipBytes = Task.Run(async () => await OCORE.zip.filesToZipArchive(fls)).Result; + stream!.Write(zipBytes, 0, zipBytes.Length); + stream.Position = 0; + return archiveFile; } catch (Exception ex) { diff --git a/Fuchs_DataService/FdsShared.cs b/Fuchs_DataService/FdsShared.cs index 3bae247..4d8c095 100644 --- a/Fuchs_DataService/FdsShared.cs +++ b/Fuchs_DataService/FdsShared.cs @@ -2,29 +2,22 @@ using System.Globalization; using System.Text; using System.Text.RegularExpressions; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; namespace fds; /// -/// Holds the application built from appsettings.json. -/// Call once at startup before accessing . +/// Holds the application supplied by the host (the Fuchs web app). +/// Fuchs_DataService is a library — it has no configuration of its own; the host owns the +/// connection strings and MFR credentials and injects them via , +/// which Program.cs calls once at startup before any use. /// public static class FdsConfig { private static IConfiguration? _config; internal static IConfiguration Current => - _config ?? throw new InvalidOperationException("FdsConfig has not been initialized. Call FdsConfig.Initialize() in Main()."); - - public static void Initialize() - { - _config = new ConfigurationBuilder() - .SetBasePath(AppDomain.CurrentDomain.BaseDirectory) - .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .Build(); - } + _config ?? throw new InvalidOperationException("FdsConfig has not been initialized. Call FdsConfig.Initialize(configuration) at host startup."); public static void Initialize(IConfiguration configuration) { @@ -32,21 +25,11 @@ public static class FdsConfig } // -- Connection strings --------------------------------------------------- - internal static string SQLConnectionString() => - Current.GetConnectionString("fuchs_ConnectionString") - ?? throw new InvalidOperationException("Missing connection string: fuchs_ConnectionString"); - internal static string FDSConnectionString() => Current.GetConnectionString("fuchs_fds_ConnectionString") ?? throw new InvalidOperationException("Missing connection string: fuchs_fds_ConnectionString"); - // -- App settings --------------------------------------------------------- - internal static double ExecutionFrequency_Minutes => - Current.GetValue("Fds:ExecutionFrequency_Minutes", 15); - - internal static bool DebugDetails => - Current.GetValue("Fds:DebugDetails"); - + // -- MFR credentials (supplied by the host from Key Vault / appsettings) -- internal static string MFR_UserName => Current["Fds:MFR_UserName"] ?? ""; @@ -59,12 +42,8 @@ public static class FdsConfig internal static class FdsShared { - internal static string SQLConnectionString() => FdsConfig.SQLConnectionString(); internal static string FDSConnectionString() => FdsConfig.FDSConnectionString(); - internal static SqlConnection SqlCon() => - new(FdsConfig.SQLConnectionString()); - public static string RandomString(byte length) { var r = new Random(); diff --git a/Fuchs_DataService/FdsZip.cs b/Fuchs_DataService/FdsZip.cs deleted file mode 100644 index c2a316c..0000000 --- a/Fuchs_DataService/FdsZip.cs +++ /dev/null @@ -1,243 +0,0 @@ -using Microsoft.Extensions.Logging; -using SevenZip; - -namespace fds; - -public class Archive : IDisposable -{ - public event Action? FileSaved; - public event Action? FileStreamCreated; - - private FileInfo _archiveFile; - private string _archivePassword; - private OutArchiveFormat _archiveFormat; - private readonly ILogger _logger; - public string TempPath { get; set; } = AppDomain.CurrentDomain.BaseDirectory; - public Stream? ArchiveFileStream { get; set; } - - private SevenZipExtractor? _zipOut; - private SevenZipCompressor? _zipIn; - public bool ZipAppend { get; set; } = true; - public bool ExitOK { get; set; } - public bool ZipInOK { get; set; } - - public Archive(FileInfo archiveFile, string archivePassword = "", bool init = true, - OutArchiveFormat type = OutArchiveFormat.SevenZip, ILogger? logger = null) - { - _logger = logger ?? Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; - _archiveFormat = type; - _archiveFile = new FileInfo(archiveFile.FullName.Replace(archiveFile.Extension, - type == OutArchiveFormat.SevenZip ? ".7z" : archiveFile.Extension)); - _archivePassword = archivePassword; - if (init) InitZipIn(type); - } - - private void InitZipIn(OutArchiveFormat type) - { - if (string.IsNullOrEmpty(Zipping.SevenZipPath)) - { - try - { - var assemblyDir = new DirectoryInfo( - new Uri(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)!).LocalPath); - var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault(); - Zipping.SevenZipPath = zip?.FullName ?? ""; - } - finally - { - if (string.IsNullOrEmpty(Zipping.SevenZipPath)) - { - var assemblyDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); - var zip = assemblyDir.GetFiles("7z.dll", SearchOption.AllDirectories).FirstOrDefault(); - Zipping.SevenZipPath = zip?.FullName ?? ""; - } - } - if (string.IsNullOrEmpty(Zipping.SevenZipPath)) - _logger.LogError("SevenZipPath not found — 7z.dll is missing from the output directory."); - } - SevenZipCompressor.SetLibraryPath(Zipping.SevenZipPath); - - _zipIn = new SevenZipCompressor - { - ArchiveFormat = (type == OutArchiveFormat.SevenZip && _archiveFile.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase)) - ? OutArchiveFormat.SevenZip : type, - CompressionLevel = SevenZip.CompressionLevel.Ultra, - CompressionMode = ZipAppend ? SevenZip.CompressionMode.Append : SevenZip.CompressionMode.Create, - DirectoryStructure = false - }; - - _zipIn.CompressionMethod = _zipIn.ArchiveFormat switch - { - OutArchiveFormat.SevenZip => CompressionMethod.Lzma2, - OutArchiveFormat.Zip or OutArchiveFormat.GZip => CompressionMethod.Deflate, - _ => CompressionMethod.Default - }; - - ZipInOK = true; - } - - public void Extract(FileInfo dataArchiveFilePath, DirectoryInfo tgtDirectory, OutArchiveFormat type = default) - { - if (!dataArchiveFilePath.Exists) return; - - if (type == default) - type = dataArchiveFilePath.Extension.Contains("7z", StringComparison.OrdinalIgnoreCase) - ? OutArchiveFormat.SevenZip : OutArchiveFormat.Zip; - - if (!ZipInOK) InitZipIn(type); - - _zipOut = string.IsNullOrEmpty(_archivePassword) - ? new SevenZipExtractor(dataArchiveFilePath.FullName) - : new SevenZipExtractor(dataArchiveFilePath.FullName, _archivePassword); - - try - { - if (!_zipOut.ArchiveFileData[0].Encrypted && !string.IsNullOrEmpty(_archivePassword)) - _archivePassword = ""; - } - catch (Exception ex) - { - _logger.LogError(ex, "Archive.Extract failed — path={Path}, target={Target}", - dataArchiveFilePath.FullName, tgtDirectory.FullName); - return; - } - - _zipOut.ExtractArchive(tgtDirectory.FullName); - _zipOut.Dispose(); - } - - public bool Compress(List files, FileInfo? archiveFile = null, string? archivePass = null, OutArchiveFormat type = OutArchiveFormat.SevenZip) - { - if (files.Count == 0) return true; - if (!ZipInOK) InitZipIn(type); - - archiveFile ??= _archiveFile; - archivePass = string.IsNullOrEmpty(archivePass) ? _archivePassword : archivePass; - - if (archiveFile.Exists && ZipAppend) - _zipIn!.CompressionMode = CompressionMode.Append; - else - { - if (archiveFile.Exists) archiveFile.Delete(); - _zipIn!.CompressionMode = CompressionMode.Create; - } - - try - { - var filesVerified = files.Where(f => f.Exists).ToArray(); - var filePaths = filesVerified.Select(f => f.FullName).ToArray(); - if (string.IsNullOrEmpty(archivePass)) - _zipIn.CompressFiles(archiveFile.FullName, filePaths); - else - { - _zipIn.EncryptHeaders = true; - _zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256; - _zipIn.CompressFilesEncrypted(archiveFile.FullName, archivePass, filePaths); - } - FileSaved?.Invoke(); - ExitOK = true; - _zipIn = null; - ZipInOK = false; - } - catch (Exception ex) - { - // Previously silent: callers (e.g. HandleDatevZip) saw only a bare failed result - // with no way to tell a compression error from any other reason ExitOK is false. - _logger.LogError(ex, "Archive.Compress failed — archiveFile={ArchiveFile} fileCount={FileCount}", - archiveFile.FullName, files.Count); - ExitOK = false; - } - archiveFile.Refresh(); - return ExitOK && archiveFile.Exists; - } - - public bool CompressToStream(Dictionary files, Stream? targetstream = null) - { - if (files.Count == 0) return true; - if (!ZipInOK) InitZipIn(_archiveFormat); - - if (ArchiveFileStream == null) - { - _zipIn!.CompressionMode = CompressionMode.Create; - ArchiveFileStream = new MemoryStream(); - } - else - _zipIn!.CompressionMode = CompressionMode.Append; - - try - { - var filesStreams = files.ToDictionary(kv => kv.Key, kv => (Stream)new MemoryStream(kv.Value)); - var target = targetstream ?? ArchiveFileStream; - if (string.IsNullOrEmpty(_archivePassword)) - _zipIn.CompressStreamDictionary(filesStreams, target); - else - { - _zipIn.EncryptHeaders = true; - _zipIn.ZipEncryptionMethod = ZipEncryptionMethod.Aes256; - _zipIn.CompressStreamDictionary(filesStreams, target, _archivePassword); - } - ArchiveFileStream.Seek(0, SeekOrigin.Begin); - FileStreamCreated?.Invoke(); - ExitOK = true; - _zipIn = null; - ZipInOK = false; - } - catch (Exception ex) - { - // Previously silent: the DATEV export ZIP-to-stream path failed with no - // trace anywhere, only a bare false return. - _logger.LogError(ex, "Archive.CompressToStream failed — fileCount={FileCount}", files.Count); - ExitOK = false; - } - return ExitOK; - } - - public bool WriteArchiveStreamToDisk(FileInfo? archiveFile = null) - { - try { if (_archiveFile.Exists) _archiveFile.Delete(); } catch { } - FdsShared.WriteStreamToDisk(ArchiveFileStream!, (archiveFile ?? _archiveFile).FullName); - _archiveFile.Refresh(); - return _archiveFile.Exists; - } - - #region IDisposable - private bool _disposed; - protected virtual void Dispose(bool disposing) - { - if (!_disposed) - { - if (disposing) - { - try - { - ArchiveFileStream?.Dispose(); - _zipOut?.Dispose(); - _zipIn = null; - } - catch { } - } - _disposed = true; - } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - #endregion -} - -public static class Zipping -{ - public static string SevenZipPath = ""; - - public static void FastAppend(FileInfo fileToZip, FileInfo archiveFile) - { - if (fileToZip.Exists && archiveFile?.Exists == true) - { - using var zip = new Archive(archiveFile) { ZipAppend = true }; - zip.Compress(new List { fileToZip }); - } - } -} diff --git a/Fuchs_DataService/Fuchs_DataService.csproj b/Fuchs_DataService/Fuchs_DataService.csproj index 65c3478..72fcb9d 100644 --- a/Fuchs_DataService/Fuchs_DataService.csproj +++ b/Fuchs_DataService/Fuchs_DataService.csproj @@ -1,7 +1,6 @@ - + net10.0 - Exe fds enable enable @@ -19,29 +18,18 @@ 1591;NU1608 - - PreserveNewest - - - + + - - - Always - - - - - - - + + diff --git a/Fuchs_DataService/Fuchs_DataService.vbproj b/Fuchs_DataService/Fuchs_DataService.vbproj deleted file mode 100644 index a3f6532..0000000 --- a/Fuchs_DataService/Fuchs_DataService.vbproj +++ /dev/null @@ -1,76 +0,0 @@ - - - net10.0 - Exe - Sub Main - fds - Empty - false - db-dev.processweb.de;Debug;Release;server02.processweb.de - - - - - - - - - x64 - Fuchs_DataService.xml - 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 - - - false - Fuchs_DataService.xml - 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 - - - - True - Application.myapp - True - - - True - True - Resources.resx - - - True - Settings.settings - True - - - - - - MyApplicationCodeGenerator - Application.Designer.vb - - - - - - - - - - - - Always - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Fuchs_DataService/Fuchs_DataService.xml b/Fuchs_DataService/Fuchs_DataService.xml index 4f31510..ed2c211 100644 --- a/Fuchs_DataService/Fuchs_DataService.xml +++ b/Fuchs_DataService/Fuchs_DataService.xml @@ -18,38 +18,10 @@ - Holds the application built from appsettings.json. - Call once at startup before accessing . - - - - - Writes log entries to Debug output and a rolling file. - Database logging is wired up but disabled — set to true to activate. - - - - Set to true to activate database logging via fds__admin_logdebug. - - - - Prepared DB logging via fds__admin_logdebug. - Enable by setting = true. - - - - - Defines a named job with its own execution schedule for use with . - - - - - Defines a named job with its own execution schedule for use with . - - - - - A that runs multiple independent jobs, each on its own . + Holds the application supplied by the host (the Fuchs web app). + Fuchs_DataService is a library — it has no configuration of its own; the host owns the + connection strings and MFR credentials and injects them via , + which Program.cs calls once at startup before any use. diff --git a/Fuchs_DataService/Logging/FdsLoggerProvider.cs b/Fuchs_DataService/Logging/FdsLoggerProvider.cs deleted file mode 100644 index 429dcc9..0000000 --- a/Fuchs_DataService/Logging/FdsLoggerProvider.cs +++ /dev/null @@ -1,117 +0,0 @@ -using Microsoft.Extensions.Logging; -using System.Diagnostics; - -namespace fds.Logging; - -/// -/// Writes log entries to Debug output and a rolling file. -/// Database logging is wired up but disabled — set to true to activate. -/// -public sealed class FdsLoggerProvider : ILoggerProvider -{ - private readonly string _logDirectory; - - /// Set to true to activate database logging via fds__admin_logdebug. - public static bool DatabaseLoggingEnabled { get; set; } = false; - - public FdsLoggerProvider(string? logDirectory = null) - { - _logDirectory = logDirectory - ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tmp"); - Directory.CreateDirectory(_logDirectory); - } - - public ILogger CreateLogger(string categoryName) => - new FdsLogger(categoryName, _logDirectory); - - public void Dispose() { } -} - -internal sealed class FdsLogger : ILogger -{ - private readonly string _categoryName; - private readonly string _logDirectory; - private static readonly Lock _fileLock = new(); - - internal FdsLogger(string categoryName, string logDirectory) - { - _categoryName = categoryName; - _logDirectory = logDirectory; - } - - public IDisposable? BeginScope(TState state) where TState : notnull => null; - - public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; - - public void Log(LogLevel logLevel, EventId eventId, TState state, - Exception? exception, Func formatter) - { - if (!IsEnabled(logLevel)) return; - - string message = formatter(state, exception); - string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - string levelTag = logLevel switch - { - LogLevel.Trace => "TRC", - LogLevel.Debug => "DBG", - LogLevel.Information => "INF", - LogLevel.Warning => "WRN", - LogLevel.Error => "ERR", - LogLevel.Critical => "CRT", - _ => "???" - }; - - string line = $"{timestamp} [{levelTag}] {_categoryName}: {message}"; - if (exception != null) - line += $"\r\n Exception: {exception.Message}\r\n Stack: {exception.StackTrace}"; - - // Always emit to Debug output - Debug.WriteLine(line); - - // Always write to file - string filename = logLevel >= LogLevel.Error ? "ErrorLog.txt" : "DebugLog.txt"; - AppendToFile(filename, line); - - // Database logging — prepared, not activated - if (FdsLoggerProvider.DatabaseLoggingEnabled) - WriteToDatabase(_categoryName, message, exception); - } - - private void AppendToFile(string filename, string line) - { - try - { - lock (_fileLock) - File.AppendAllText(Path.Combine(_logDirectory, filename), line + "\r\n"); - } - catch { /* never throw from logger */ } - } - - /// - /// Prepared DB logging via fds__admin_logdebug. - /// Enable by setting = true. - /// - private static void WriteToDatabase(string codeReference, string message, Exception? exception) - { - // Activate by setting FdsLoggerProvider.DatabaseLoggingEnabled = true in appsettings / startup. - // - // using var con = new Microsoft.Data.SqlClient.SqlConnection(FdsConfig.FDSConnectionString()); - // using var cmd = new Microsoft.Data.SqlClient.SqlCommand( - // "EXECUTE [dbo].[fds__admin_logdebug] @CodeReference, @ExceptionMessage, @StackTrace, @Data;", con); - // cmd.Parameters.AddWithValue("@CodeReference", codeReference); - // cmd.Parameters.AddWithValue("@ExceptionMessage", (object?)exception?.Message ?? DBNull.Value); - // cmd.Parameters.AddWithValue("@StackTrace", (object?)exception?.StackTrace ?? DBNull.Value); - // cmd.Parameters.AddWithValue("@Data", message); - // con.Open(); - // cmd.ExecuteNonQuery(); - } -} - -public static class FdsLoggingExtensions -{ - public static ILoggingBuilder AddFdsLogging(this ILoggingBuilder builder, string? logDirectory = null) - { - builder.AddProvider(new FdsLoggerProvider(logDirectory)); - return builder; - } -} diff --git a/Fuchs_DataService/appsettings.json b/Fuchs_DataService/appsettings.json deleted file mode 100644 index 7a7f01c..0000000 --- a/Fuchs_DataService/appsettings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ConnectionStrings": { - "fuchs_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_web;password='Bt5pL/cJg9oxb5';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;", - "fuchs_fds_ConnectionString": "Data Source=MSSQL4.NBG4.DOMAINXYZ.DE,10439;Initial Catalog=site_fuchs_dev;Persist Security Info=False;TrustServerCertificate=true;Encrypt=true;User ID=fuchs_fds;password='!Po@cGZ5bUn37khO';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;" - }, - "Fds": { - "ExecutionFrequency_Minutes": 15, - "DebugDetails": true, - "MFR_UserName": "system@sebastian-fuchs---bad-und-heizung-gmbh-und-co-kg.com", - "MFR_Password": "0oT4G3H2", - "MFR_host": "portal.mobilefieldreport.com" - } -} \ No newline at end of file diff --git a/Fuchs_DataService/install.bat b/Fuchs_DataService/install.bat deleted file mode 100644 index bd9c444..0000000 --- a/Fuchs_DataService/install.bat +++ /dev/null @@ -1 +0,0 @@ -Fuchs_Dataservice.exe install --autostart \ No newline at end of file diff --git a/Fuchs_DataService/un-install.bat b/Fuchs_DataService/un-install.bat deleted file mode 100644 index 1312ccc..0000000 --- a/Fuchs_DataService/un-install.bat +++ /dev/null @@ -1 +0,0 @@ -Fuchs_Dataservice.exe uninstall \ No newline at end of file diff --git a/Fuchs_Database/FuchsDatabase.sqlproj b/Fuchs_Database/FuchsDatabase.sqlproj index cd90244..d5b509d 100644 --- a/Fuchs_Database/FuchsDatabase.sqlproj +++ b/Fuchs_Database/FuchsDatabase.sqlproj @@ -357,31 +357,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -424,5 +399,8 @@ + + +
        \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__createCredit_simple.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__createCredit_simple.sql deleted file mode 100644 index 92d34c5..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__createCredit_simple.sql +++ /dev/null @@ -1,264 +0,0 @@ - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__createCredit_simple] - @InvId varchar(20) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__createCredit_simple' ,@authuser , 'invid: ' + ISNULL(@InvId, '')); - - DECLARE @now datetime = GETDATE(); - DECLARE @today date = CAST(@now as date); - - DECLARE @newid varchar(10) = [dbo].[fds__fn_invoice_id](); - - - DECLARE @inv_DocumentName varchar(50), @inv_DateFinalized datetime, @inv_vat numeric(5,2), @inv_net numeric(10,3); - - IF EXISTS (SELECT 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1) - BEGIN - - SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateFinalized], @inv_vat = [InvoiceVAT_1], @inv_net = 1 - FROM [dbo].[fds__invoices] WHERE [id] = @InvId - - INSERT INTO [dbo].[fds__invoices] - ([Id] - ,[Version] - ,[InvoiceId] - ,[InvoiceType] - ,[InvoiceTitle] - ,[DocumentName] - ,[InvoiceBalance] - ,[InvoiceBalance_net] - ,[InvoiceVAT_net1] - ,[InvoiceVAT_1] - ,[InvoiceVAT_net2] - ,[InvoiceVAT_2] - ,[PaymentTerm] - ,[DueDate] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[ProvisionPeriod] - ,[ProvisionLocation] - ,[PaymentStatus] - ,[IsPayed] - ,[IsSent] - ,[Replaces_InvId] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCancelled] - ,[UserCancelled] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified]) - SELECT - [Id] = @newid - ,[Version] = 0 - ,[InvoiceId] = NULL - ,[InvoiceType] = 'g' - ,[InvoiceTitle] = 'Gutschrift' - ,[DocumentName] = NULL - ,[InvoiceBalance] = (@inv_net * -1) * (1 + 0.01 * ISNULL(@inv_vat,19)) - ,[InvoiceBalance_net] = (@inv_net * -1) - ,[InvoiceVAT_net1] = (@inv_net * -1) * (0.01 * ISNULL(@inv_vat,19)) - ,[InvoiceVAT_1] = @inv_net - ,[InvoiceVAT_net2] = null - ,[InvoiceVAT_2] = null - ,[PaymentTerm] - ,[DueDate] = @today - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[ProvisionPeriod] - ,[ProvisionLocation] - ,[PaymentStatus] = NULL - ,[IsPayed] = 0 - ,[IsSent] = 0 - ,[Replaces_InvId] = @InvId - ,[CustomValues] - ,[DateSent] = NULL - ,[UserSent] = @authuser - ,[DateFinalized] = NULL - ,[UserFinalized] = NULL - ,[DateCancelled] = NULL - ,[UserCancelled] = NULL - ,[DateCreated] = @now - ,[UserCreated] = @authuser - ,[DateModified] = @now - ,[UserModified] = @authuser - FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1; - END - ELSE IF EXISTS (SELECT 0 FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice')) - BEGIN - - SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateOfCreation] - , @inv_vat = CASE WHEN ISNULL([InvoiceBalanceNetto], 0) <> 0 THEN (([InvoiceBalance] / [InvoiceBalanceNetto]) - 1)*100 ELSE (SELECT TOP(1) TRY_CAST([value] as numeric(5,2)) FROM [dbo].[fds__admin_settings] as st where st.[type] = 'defaults' and st.[key] = 'vat') END - , @inv_net = 1 --[InvoiceBalanceNetto] - FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice'); - - INSERT INTO [dbo].[fds__invoices] - ([Id] - ,[Version] - ,[InvoiceId] - ,[InvoiceType] - ,[InvoiceTitle] - ,[DocumentName] - ,[InvoiceBalance] - ,[InvoiceBalance_net] - ,[InvoiceVAT_net1] - ,[InvoiceVAT_1] - ,[InvoiceVAT_net2] - ,[InvoiceVAT_2] - ,[PaymentTerm] - ,[DueDate] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[ProvisionPeriod] - ,[ProvisionLocation] - ,[PaymentStatus] - ,[IsPayed] - ,[IsSent] - ,[Replaces_InvId] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCancelled] - ,[UserCancelled] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified]) - SELECT - [Id] = @newid - ,[Version] = 0 - ,[InvoiceId] = NULL - ,[InvoiceType] = 'g' - ,[InvoiceTitle] = 'Gutschrift' - ,[DocumentName] = NULL - ,[InvoiceBalance] = (@inv_net * -1) * (1 + 0.01 * ISNULL(@inv_vat,19)) - ,[InvoiceBalance_net] = (@inv_net * -1) - ,[InvoiceVAT_net1] = (@inv_net * -1) * (0.01 * ISNULL(@inv_vat,19)) - ,[InvoiceVAT_1] = @inv_vat - ,[InvoiceVAT_net2] = NULL - ,[InvoiceVAT_2] = NULL - ,[PaymentTerm] = '10wd' - ,[DueDate] = _i.DueDate - ,[CustomerId] = (SELECT TOP(1) [partnerid] from [mfr__*PartnerSet] as ps where ps.[Property] = 'Invoice:Customer' and ps.[EntityId] = _i.[id]) - ,[SendToAddress] = NULL - ,[SendToEmail] = NULL - ,[ProvisionPeriod] = null - ,[ProvisionLocation] = NULL - ,[PaymentStatus] = NULL - ,[IsPayed] = 0 - ,[IsSent] = 0 - ,[Replaces_InvId] = @InvId - ,[CustomValues] = null - ,[DateSent] = NULL - ,[UserSent] = @authuser - ,[DateFinalized] = NULL - ,[UserFinalized] = NULL - ,[DateCancelled] = NULL - ,[UserCancelled] = NULL - ,[DateCreated] = @now - ,[UserCreated] = @authuser - ,[DateModified] = @now - ,[UserModified] = @authuser - FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice'); - UPDATE [dbo].[fds__invoices] - SET [SendToAddress] = [dbo].[fds__getCompanyNameAddress]([CustomerId]), [SendToEmail] = [dbo].[fds__getCompanyEmail]([CustomerId]) - WHERE [Id] = @newid; - - END - - - - -- continue creating storno - - DECLARE @inv_srq [dbo].[fds__tt__invoice_servicerequests]; - INSERT INTO [dbo].[fds__invoice_servicerequests] - ([InvId] - ,[mfr__servicerequest] - ,[title] - ,[value_net] - ,[SortOrder]) - OUTPUT - inserted.* INTO @inv_srq - SELECT - [InvId] = [id] - ,NULL -- - ,'Gutschrift' - ,(@inv_net * -1) - ,0 - FROM [dbo].[fds__invoices] WHERE [id] = @newid; - - - INSERT INTO [dbo].[fds__invoice_items] - ([InvId] - ,[InvRqId] - ,[mfr__item] - ,[reference_InvId] - ,[Type] - ,[Position] - ,[Quantity] - ,[Text] - ,[value] - ,[value_total] - ,[vat] - ,[value_service] - ,[det] - ,[SortOrder]) - SELECT - @newid -- - ,[id]-- - ,NULL -- - ,@InvId -- - ,'CreditInvoice' -- - ,NULL -- - ,'1' --- - ,[Text] = '

        ' + 'Gutschrift zu der ' + REPLACE(@inv_DocumentName, '.pdf','') + ' vom ' + FORMAT(@inv_dateFinalized, 'dd.MM.yyyy', 'de-de') + '

        ' - ,[value] = (@inv_net * -1) - ,[value_total] = (@inv_net * -1) - ,[vat] = @inv_vat - ,0 -- - ,1 -- - ,1 --) - FROM @inv_srq; - - - INSERT INTO [dbo].[fds__invoice_details] - ([InvId] - ,[InvoiceService_net] - ,[InvoiceService_VAT] - ,[AllocatedTo_InvId] - ,[StornoTo_InvId]) - VALUES( - @newid - ,NULL --[InvoiceService_net] - ,NULL --[InvoiceService_VAT] - ,NULL --[AllocatedTo_InvId] - ,@InvId --[StornoTo_InvId] - ); - - EXECUTE [dbo].[fds__getInvoice] @newid, @authuser; - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__createReminder.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__createReminder.sql deleted file mode 100644 index b16b388..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__createReminder.sql +++ /dev/null @@ -1,120 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__createReminder] - @InvId varchar(15) - , @type varchar(3) - , @amount numeric(10,3) - , @amount_payed numeric(10,3) - , @SendToAddress [nvarchar](1000) - , @SendToEmail [nvarchar](255) - , @subject nvarchar(255) - , @text nvarchar(2000) - , @authuser varchar(25) - , @Id varchar(10) OUT - -AS -BEGIN - SET NOCOUNT ON; - - - IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__createReminder' ,@authuser , 'invid: ' + ISNULL(@invid, '')); - - DECLARE @now datetime = GETUTCDATE(); - DECLARE @today date = @now; - - - DECLARE @newid varchar(10) = [dbo].[fds__fn_reminder_id](); - - DECLARE @OUT [dbo].[fds__tt__reminder_core]; - DECLARE @CustomerId bigint = dbo.fds__fn_invoice_customerid(@InvId); - - INSERT INTO [dbo].[fds__reminder] - ([Id] - ,[version] - ,[InvId] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[type] - ,[amount] - ,[amount_payed] - ,[subject] - ,[text] - ,[IsSent] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified] - ,[file]) - OUTPUT inserted.* - INTO @OUT - VALUES - (@newid - ,0 --version - ,@InvId - ,@CustomerId - ,@SendToAddress - ,@SendToEmail - ,@type - ,@amount - ,@amount_payed - ,@subject - ,@text - ,0 --IsSent, bit,> - ,null -- - ,NULL -- - ,NULL --[UserSent] - ,NULL -- - ,NULL -- - ,@now - ,@authuser - ,@now - ,@authuser - ,NULL); - - - SELECT TOP(1) @Id = [id] FROM @out; - - - With inv1 as( - select TOP(1) * FROM [dbo].[fds__invoices] WHERE [id] = @InvId - ), inv as( - SELECT [Id] = cast([id] as varchar(15)) - ,[Invoiceid] - ,[DateFinalized] - ,[InvoiceBalance] - FROM inv1 - UNION - SELECT [Id] = cast([id] as varchar(15)) - ,[Invoiceid] - ,[DateFinalized] = [DateOfCreation] - ,[InvoiceBalance] - FROM [dbo].[mfr__invoices] as mfri - WHERE [id] = TRY_CAST(@invId as bigint) AND NOT EXISTS( SELECT 0 FROM inv1 ) --will only be used, if id not present in fds__invoices - ) - SELECT TOP(1) - o.* - , [InvoiceId] = inv.[InvoiceId] - , [InvoiceDate] = inv.[DateFinalized] - , [hasFile] = CAST ( CASE WHEN o.[file] is null THEN 0 ELSE 1 END as bit) - , [UserNameFinalized] = [dbo].[fis_admin_getUserName_byID](o.[UserFinalized]) - , [UserEmailFinalized] = [dbo].[fis_admin_getUserEmail_byID](o.[UserFinalized]) - from @out as o - join inv on o.[invid] = inv.[id]; - - - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__createStorno_simple.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__createStorno_simple.sql deleted file mode 100644 index 7eb58b0..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__createStorno_simple.sql +++ /dev/null @@ -1,263 +0,0 @@ - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__createStorno_simple] - @InvId varchar(20) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__createStorno_simple' ,@authuser , 'invid: ' + ISNULL(@InvId, '')); - - DECLARE @now datetime = GETDATE(); - DECLARE @today date = CAST(@now as date); - - DECLARE @newid varchar(10) = [dbo].[fds__fn_invoice_id](); - - - DECLARE @inv_DocumentName varchar(50), @inv_DateFinalized datetime, @inv_vat numeric(5,2), @inv_net numeric(10,3); - - IF EXISTS (SELECT 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1) - BEGIN - - SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateFinalized], @inv_vat = [InvoiceVAT_1], @inv_net = [invoicebalance_net] FROM [dbo].[fds__invoices] WHERE [id] = @InvId - - INSERT INTO [dbo].[fds__invoices] - ([Id] - ,[Version] - ,[InvoiceId] - ,[InvoiceType] - ,[InvoiceTitle] - ,[DocumentName] - ,[InvoiceBalance] - ,[InvoiceBalance_net] - ,[InvoiceVAT_net1] - ,[InvoiceVAT_1] - ,[InvoiceVAT_net2] - ,[InvoiceVAT_2] - ,[PaymentTerm] - ,[DueDate] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[ProvisionPeriod] - ,[ProvisionLocation] - ,[PaymentStatus] - ,[IsPayed] - ,[IsSent] - ,[Replaces_InvId] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCancelled] - ,[UserCancelled] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified]) - SELECT - [Id] = @newid - ,[Version] = 0 - ,[InvoiceId] = NULL - ,[InvoiceType] = 'c' - ,[InvoiceTitle] = 'Stornorechnung' - ,[DocumentName] = NULL - ,[InvoiceBalance] = ([InvoiceBalance] * -1) - ,[InvoiceBalance_net] = ([InvoiceBalance_net] *-1) - ,[InvoiceVAT_net1] = ([InvoiceVAT_net1] * -1) - ,[InvoiceVAT_1] - ,[InvoiceVAT_net2] = ([InvoiceVAT_net2] * -1) - ,[InvoiceVAT_2] - ,[PaymentTerm] - ,[DueDate] = @today - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[ProvisionPeriod] - ,[ProvisionLocation] - ,[PaymentStatus] = NULL - ,[IsPayed] = 0 - ,[IsSent] = 0 - ,[Replaces_InvId] = @InvId - ,[CustomValues] - ,[DateSent] = NULL - ,[UserSent] = NULL - ,[DateFinalized] = NULL - ,[UserFinalized] = NULL - ,[DateCancelled] = NULL - ,[UserCancelled] = NULL - ,[DateCreated] = @now - ,[UserCreated] = @authuser - ,[DateModified] = @now - ,[UserModified] = @authuser - FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1; - END - ELSE IF EXISTS (SELECT 0 FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice')) - BEGIN - - SELECT @inv_DocumentName = [documentname], @inv_DateFinalized = [DateOfCreation] - , @inv_vat = CASE WHEN ISNULL([InvoiceBalanceNetto], 0) <> 0 THEN (([InvoiceBalance] / [InvoiceBalanceNetto]) - 1)*100 ELSE (SELECT TOP(1) TRY_CAST([value] as numeric(5,2)) FROM [dbo].[fds__admin_settings] as st where st.[type] = 'defaults' and st.[key] = 'vat') END - , @inv_net = [InvoiceBalanceNetto] - FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice'); - - INSERT INTO [dbo].[fds__invoices] - ([Id] - ,[Version] - ,[InvoiceId] - ,[InvoiceType] - ,[InvoiceTitle] - ,[DocumentName] - ,[InvoiceBalance] - ,[InvoiceBalance_net] - ,[InvoiceVAT_net1] - ,[InvoiceVAT_1] - ,[InvoiceVAT_net2] - ,[InvoiceVAT_2] - ,[PaymentTerm] - ,[DueDate] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[ProvisionPeriod] - ,[ProvisionLocation] - ,[PaymentStatus] - ,[IsPayed] - ,[IsSent] - ,[Replaces_InvId] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCancelled] - ,[UserCancelled] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified]) - SELECT - [Id] = @newid - ,[Version] = 0 - ,[InvoiceId] = NULL - ,[InvoiceType] = 'c' - ,[InvoiceTitle] = 'Stornorechnung' - ,[DocumentName] = NULL - ,[InvoiceBalance] = ([InvoiceBalance] * -1) - ,[InvoiceBalance_net] = ([InvoiceBalanceNetto] *-1) - ,[InvoiceVAT_net1] = ((ISNULL([InvoiceBalance],0) - ISNULL([InvoiceBalanceNetto],0)) * -1) - ,[InvoiceVAT_1] = @inv_vat - ,[InvoiceVAT_net2] = NULL - ,[InvoiceVAT_2] = NULL - ,[PaymentTerm] = '10wd' - ,[DueDate] = _i.DueDate - ,[CustomerId] = (SELECT TOP(1) [partnerid] from [mfr__*PartnerSet] as ps where ps.[Property] = 'Invoice:Customer' and ps.[EntityId] = _i.[id]) - ,[SendToAddress] = NULL - ,[SendToEmail] = NULL - ,[ProvisionPeriod] = null - ,[ProvisionLocation] = NULL - ,[PaymentStatus] = NULL - ,[IsPayed] = 0 - ,[IsSent] = 0 - ,[Replaces_InvId] = @InvId - ,[CustomValues] = null - ,[DateSent] = NULL - ,[UserSent] = NULL - ,[DateFinalized] = NULL - ,[UserFinalized] = NULL - ,[DateCancelled] = NULL - ,[UserCancelled] = NULL - ,[DateCreated] = @now - ,[UserCreated] = @authuser - ,[DateModified] = @now - ,[UserModified] = @authuser - FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice'); - UPDATE [dbo].[fds__invoices] - SET [SendToAddress] = [dbo].[fds__getCompanyNameAddress]([CustomerId]), [SendToEmail] = [dbo].[fds__getCompanyEmail]([CustomerId]) - WHERE [Id] = @newid; - - END - - - - -- continue creating storno - - DECLARE @inv_srq [dbo].[fds__tt__invoice_servicerequests]; - INSERT INTO [dbo].[fds__invoice_servicerequests] - ([InvId] - ,[mfr__servicerequest] - ,[title] - ,[value_net] - ,[SortOrder]) - OUTPUT - inserted.* INTO @inv_srq - SELECT TOP(1) - [InvId] = [id] - ,NULL -- - ,'Storno' - ,0 - ,0 - FROM [dbo].[fds__invoices] WHERE [id] = @newid; - - - INSERT INTO [dbo].[fds__invoice_items] - ([InvId] - ,[InvRqId] - ,[mfr__item] - ,[reference_InvId] - ,[Type] - ,[Position] - ,[Quantity] - ,[Text] - ,[value] - ,[value_total] - ,[vat] - ,[value_service] - ,[det] - ,[SortOrder]) - SELECT - @newid -- - ,[id]-- - ,NULL -- - ,@InvId -- - ,'CanceledInvoice' -- - ,NULL -- - ,'1' --- - ,[Text] = '

        ' + 'Storno der ' + REPLACE(@inv_DocumentName, '.pdf','') + ' vom ' + FORMAT(@inv_dateFinalized, 'dd.MM.yyyy', 'de-de') + '

        ' - ,[value] = (@inv_net * -1) - ,[value_total] = (@inv_net * -1) - ,[vat] = @inv_vat - ,0 -- - ,1 -- - ,1 --) - FROM @inv_srq; - - - INSERT INTO [dbo].[fds__invoice_details] - ([InvId] - ,[InvoiceService_net] - ,[InvoiceService_VAT] - ,[AllocatedTo_InvId] - ,[StornoTo_InvId]) - VALUES( - @newid - ,NULL --[InvoiceService_net] - ,NULL --[InvoiceService_VAT] - ,NULL --[AllocatedTo_InvId] - ,@InvId --[StornoTo_InvId] - ); - - EXECUTE [dbo].[fds__getInvoice] @newid, @authuser; - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getBankingtransfers.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getBankingtransfers.sql deleted file mode 100644 index 21dfadb..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getBankingtransfers.sql +++ /dev/null @@ -1,51 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getBankingtransfers] - @mode varchar(1) = 'm' - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_bam', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__getBankingtransfers_questionable' ,@authuser , ''); - - DECLARE @startdate date, @enddate date, @today date = GETDATE(); - DECLARE @true bit = 1, @false bit = 0; - SET @mode = LOWER(@mode); - - DECLARE @invidlist [dbo].[fds__tt__idlist_vchar]; - - --output admin - SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode, [title] = 'Zahlungen (neueste zuerst)' - ,[note] = 'Letzes Buchungsdatum: ' + FORMAT((SELECT MAX([valuedate]) from [fds__bankingtransactions]), 'dd.MM.yy'); - - - select - ic.[taID] - --inv.InvoiceId, - --[IsPayed], - , ic.fds - , InvoiceId = ISNULL(fi.[invoiceid], mi.[invoiceid]) - , [ValueDate] = FORMAT(ic.[ValueDate], 'dd.MM.yy') - , [InvoiceBalance] = FORMAT((ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0)), '0.00€', 'de') - , [Amount] = FORMAT(ic.[amount], '0.00€', 'de') - , [Skonto] = CAST(CASE WHEN ic.[amount] between (0.965 * (ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0))) and (0.975 * (ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0))) THEN 1 ELSE 0 END as bit) - , [Deviation] = FORMAT(CASE WHEN ISNULL((ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0)),0) > 9 THEN ic.[amount] / (ISNULL(fi.[InvoiceBalance],0.0) + ISNULL(mi.[InvoiceBalance],0.0)) ELSE NULL END , '0.00%') - --, ic.* - ,ic.AccountNumberOfPayer - ,ic.NameOfPayer - ,ic.SepaRemittanceInformation - ,ic.EndToEndReference - , [order] = ROW_NUMBER() OVER (ORDER BY [valuedate] DESC) - FROM - [dbo].[fds__fn_bankingtransactions](null, null, @startdate, @enddate, @invidlist) as ic - LEFT JOIN [dbo].[fds__invoices] as fi on ic.[fds] = 1 and ic.[invid] = fi.id - LEFT JOIN [dbo].[mfr__invoices] as mi on ic.[fds] = 0 and ic.[invid] = mi.id -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getBankingtransfers_questionable.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getBankingtransfers_questionable.sql deleted file mode 100644 index aa74767..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getBankingtransfers_questionable.sql +++ /dev/null @@ -1,60 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getBankingtransfers_questionable] - @mode varchar(1) = 'm' - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_bam', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__getBankingtransfers_questionable' ,@authuser , ''); - - DECLARE @startdate date, @enddate date, @today date = GETDATE(); - DECLARE @true bit = 1, @false bit = 0; - SET @mode = LOWER(@mode); - - - --output admin - SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode, [title] = 'Auffällige Zahlungen (neueste zuerst)' - ,[note] = 'Letzes Buchungsdatum: ' + FORMAT((SELECT MAX([valuedate]) from [fds__bankingtransactions]), 'dd.MM.yy'); - - - with inv as ( - SELECT [fds] = CAST(1 as bit), [InvId] = CAST([Id] as varchar(25)), [InvoiceId], [InvoiceBalance] = CAST([InvoiceBalance] as numeric(10,2)), [ispayed] FROM [dbo].[fds__invoices] as i - UNION - SELECT [fds] = CAST(0 as bit), [InvId] = CAST([Id] as varchar(25)), [InvoiceId], [InvoiceBalance] = CAST([InvoiceBalance] as numeric(10,2)), [ispayed] = CAST(CASE WHEN [InvoiceState] = 'eIsCancelled' THEN 1 WHEN [InvoiceState] = 'eIsPaid' THEN 1 ELSE 0 END as bit) FROM [dbo].[mfr__invoices] as i - ) - select - ic.[taID] - --inv.InvoiceId, - --[IsPayed], - , inv.fds - , ic.InvoiceId - , [ValueDate] = FORMAT(ic.[ValueDate], 'dd.MM.yy') - , [InvoiceBalance] = FORMAT(inv.[InvoiceBalance], '0.00€', 'de') + ISNULL( CASE WHEN id2.[invid] is not null then (SELECT TOP(1) FORMAT(inv2.[InvoiceBalance], '0.00€', 'de') FROM [dbo].[fds__invoices] as inv2 where inv2.Id = id2.[invid]) ELSE '' END ,'') - , [Amount] = FORMAT(ic.[amount], '0.00€', 'de') - , [Skonto] = CAST(CASE WHEN ic.[amount] between (0.965 * inv.[InvoiceBalance]) and (0.975 * inv.[InvoiceBalance]) THEN 1 ELSE 0 END as bit) - , [Deviation] = FORMAT(CASE WHEN ISNULL(inv.[InvoiceBalance],0) > 9 THEN ic.[amount] / inv.[InvoiceBalance] ELSE NULL END , '0.00%') - --, ic.* - ,ic.AccountNumberOfPayer - ,ic.NameOfPayer - ,ic.SepaRemittanceInformation - ,ic.EndToEndReference - , [order] = ROW_NUMBER() OVER (ORDER BY [valuedate] DESC) - FROM - [dbo].[fds__getInvoiceCredits] (null, null) as ic - LEFT JOIN inv on inv.[InvoiceId] = ic.[invoiceid] - LEFT JOIN [dbo].[fds__invoice_details] as id2 ON inv.InvId = id2.[stornoto_invid] - WHERE - ((ISNULL(inv.ispayed,0) = 0 and fds = 1) OR - ic.[amount] not between (inv.InvoiceBalance - 0.99) and (inv.InvoiceBalance + 0.99) OR - ic.invoiceid is null oR - inv.invoiceid is null - ) AND NOT EXISTS (SELECT 0 FROM [dbo].[fds__bankingtransactions_settings] as bs WHERE bs.[taID] = ic.[taID] AND (ISNULL(bs.[done_manually],'') <> '' OR ISNULL(bs.[assigned_invoice_id],'') <> '')) -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getDatevExports.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getDatevExports.sql deleted file mode 100644 index e748ef3..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getDatevExports.sql +++ /dev/null @@ -1,634 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getDatevExports] - @tgtdate date - ,@mode varchar(1) = 'm' - ,@files bit - ,@authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - - - DECLARE @startdate date, @enddate date; - SET @mode = LOWER(@mode); - If @mode = 'm' - BEGIN - SELECT @startdate = [dbo].[date_monthfirst](@tgtdate) - ,@enddate = [dbo].[date_monthend](@tgtdate); - END - ELSE If @mode = 'w' - BEGIN - SELECT @startdate = [dbo].[date_weekfirst](@tgtdate) - ,@enddate = [dbo].[date_weekend](@tgtdate); - END - - DECLARE @contraaccounts as table([account] varchar(10), [vat] numeric(5,2)); - insert into @contraaccounts - SELECT [account] = [key], [vat] = TRY_PARSE([value2] as numeric(5,2) USING 'en-US') FROM [dbo].[fds__admin_settings] WHERE [type] = 'contra_account' - - SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode - , [beraternummer] = 11653 - , [mandantennummer] = 62321 - , [WJ-Beginn] = DATEFROMPARTS(YEAR(@tgtDate), 1, 1) - , [Sachkontenlänge] = ISNULL((SELECT MAX(LEN([account])) FROM @contraaccounts), 4) - ; - - - DECLARE @inv TABLE([Id] varchar(20), [InvoiceID] varchar(255), [URI] nvarchar(255), [DocumentName] nvarchar(255), [DateOfCreation] date, [fds] bit, [file_guid] varchar(36)); - --INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__reports] where [IsInvoice] = 1 AND CAST([DateOfCreation] as date) between @startdate AND @enddate; - INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0, [file_guid] FROM [dbo].[mfr__invoices] where CAST([DateOfCreation] as date) between @startdate AND @enddate and [FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview','') ; - INSERT INTO @inv SELECT [Id],[invoiceid],[URI] = null,[DocumentName],[DateOfCreation] = [DateCreated],[fds] = 1, [file_guid] FROM [dbo].[fds__invoices] where CAST([DateCreated] as date) between @startdate AND @enddate and [isfinal] = 1; - - SELECT - inv.[Id],inv.[InvoiceID],inv.[URI],inv.[DocumentName],inv.[fds] - , [file] = CASE WHEN @files = 1 then finv.[file] ELSE NULL END - , inv.[file_guid], [DateOfCreation] - --,[order] = ROW_NUMBER() OVER (ORDER BY [DateOfCreation] ASC) - FROM @inv as inv LEFT JOIN [dbo].[fds__invoices] as finv on inv.[id] = finv.[id]; - - DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(15), [customer.ExternalId] bigint, [customer.Id] bigint, [ExternalId] nvarchar(255), [fds] bit); - INSERT INTO @SReq - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds] - FROM [dbo].[mfr__servicerequests] as s JOIN @inv as i on s.[InvoiceId] like ('%' + i.[InvoiceId] + '%') LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - WHERE i.fds = 0; - INSERT INTO @SReq - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds] - FROM [dbo].[fds__invoice_servicerequests] as irq - JOIN @inv as i on irq.[InvId] = i.[Id] - LEFT JOIN [dbo].[mfr__servicerequests] as s ON irq.[mfr__servicerequest] = s.[id] - LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - WHERE i.fds = 1; - - - - With itm as ( - SELECT - [net_itemsum] = CAST( SUM((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) as numeric(12,2)) - , [bo_itemsum] = CAST( SUM( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01))) as numeric(12,2)) - , [vf] = MAX(1 + (ISNULL(vat.[vat], 19.0) * 0.01)) - , sreq.[invoice.Id] - ,[fds] = 0 - FROM [dbo].[mfr__items] as itm - JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item' - WHERE [fds] = 0 - GROUP BY sreq.[invoice.Id] - UNION - SELECT - [net_itemsum] = CAST( SUM(ISNULL([value_total],0)) as numeric(12,2)) - , [bo_itemsum] = CAST( SUM(ISNULL([value_total],0) * (1 + (ISNULL([vat], 19.0) * 0.01))) as numeric(12,2)) - , [vf] = MAX(1 + (ISNULL([vat], 19.0) * 0.01)) - , [invoice.Id] = inv.[id] --sreq.[invoice.Id] - , [fds] = 1 - FROM [dbo].[fds__invoice_items] as itm - --JOIN @sreq as sreq ON itm.[InvRqId] = SReq.[id] - JOIN @inv as inv ON itm.[invid] = inv.[id] and [fds] = 1 - WHERE [fds] = 1 - GROUP BY inv.[id] --sreq.[invoice.Id] - ),sc as( - SELECT s.[invoice.Id], [fds], [#] = COUNT(s.[id]) FROM @SReq as s GROUP BY s.[invoice.Id], [fds] - ), inv as( - --select - -- [Id] = CAST(iv.[Id] as varchar(20)) - -- ,[InvoiceId] = iv.[InvoiceId] - -- ,[requestcount] = sc.[#] - -- ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id) - -- ,[balance] = ABS([invoiceBalance]) - -- ,[EINZELPOS_brutto] = itm.bo_itemsum - -- ,[EINZELPOS_netto] = itm.net_itemsum - -- ,[vf] - -- ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - -- ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - -- ,[contra_account] = ca.[account] - -- ,[Belegdatum] = iv.[DateOfCreation] - -- ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) - -- ,iv.[DateOfCreation] - -- from [dbo].[mfr__reports] as iv - -- LEFT JOIN itm on iv.Id = itm.[invoice.Id] - -- LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report' - -- LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0 - -- LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat] - -- where iv.Id in (SELECT [Id] FROM @inv Where fds = 0) - --UNION - select - [Id] = CAST(iv.[Id] as varchar(20)) - ,[InvoiceId] = iv.[InvoiceId] - ,[requestcount] = sc.[#] - ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id) - ,[balance] = ABS([invoiceBalance]) - ,[EINZELPOS_brutto] = itm.bo_itemsum - ,[EINZELPOS_netto] = itm.net_itemsum - ,[vf] - ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - ,[contra_account] = ca.[account] - ,[Belegdatum] = iv.[DateOfCreation] - ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) - ,iv.[DateOfCreation] - ,[file_guid] - from [dbo].[mfr__invoices] as iv - LEFT JOIN itm on iv.Id = itm.[invoice.Id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report' - LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0 - LEFT JOIN @contraaccounts as ca ON ISNULL(vat.[vat], ISNULL([dbo].[fds__fn_vatfrombalances]([invoiceBalance], [invoiceBalancenetto]),19.0)) = ca.[vat] - where iv.Id in (SELECT [Id] FROM @inv Where fds = 0) - UNION - select - iv.[Id] - ,[InvoiceId] = iv.[InvoiceId] - ,[requestcount] = sc.[#] - ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id) - ,[balance] = ABS([invoiceBalance]) - ,[EINZELPOS_brutto] = itm.bo_itemsum - ,[EINZELPOS_netto] = itm.net_itemsum - ,[vf] - ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - ,[customer] = (SELECT TOP(1) [ExternalId] FROM [dbo].[mfr__companies] as c WHERE c.[Id] = iv.[CustomerId]) - ,[contra_account] = ca.[account] - ,[Belegdatum] = iv.[DateCreated] - ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) - ,[DateOfCreation] = iv.[DateCreated] - ,[file_guid] - from [dbo].[fds__invoices] as iv - LEFT JOIN itm on iv.Id = itm.[invoice.Id] - LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 1 - LEFT JOIN @contraaccounts as ca ON ISNULL(iv.[InvoiceVAT_1], 19.0) = ca.[vat] - where iv.Id in (SELECT [Id] FROM @inv Where fds = 1) - ) - select - [Umsatz (ohne Soll/Haben-Kz)] = ABS([balance])--CAST(FORMAT(ABS([balance]), '0.00', 'de') as varchar(13)) - --,[EINZELPOS_brutto] - --,[EINZELPOS_netto] - --,vf - ,[Soll/Haben-Kennzeichen] = [dbo].[ott_quote_255](UPPER([deb_cred])) - ,[WKZ Umsatz] = [dbo].[ott_quote_255]('') - ,[Kurs] = NULL - ,[Basis-Umsatz] = NULL - ,[WKZ Basis-Umsatz] = [dbo].[ott_quote_255]('') - ,[Konto] = CAST(iv.[customer] as bigint) --CAST(iv.[customer] as varchar(9)) --(SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] ORDER BY SReq.DateOfCreation ASC) - ,[Gegenkonto (ohne BU-Schlüssel)] = CAST([contra_account] as bigint )--CAST([contra_account] as varchar(9))] - ,[BU-Schlüssel] = [dbo].[ott_quote_255]('') - ,[Belegdatum] = FORMAT(iv.[Belegdatum], 'ddMM') - ,[Belegfeld 1] = [dbo].[ott_quote_255](iv.[InvoiceId]) - ,[Belegfeld 2] = [dbo].[ott_quote_255]([dbo].[strings_removeLeading0_255](FORMAT(iv.[Belegdatum], 'ddMMyy'))) - ,[Skonto] = [dbo].[ott_quote_255]('') - ,[Buchungstext] = [dbo].[ott_quote_255](CAST(ISNULL(LEFT( TRIM(CAST((SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) as varchar(255)) ), 60) ,'') as varchar(60))) - ,[Postensperre] = NULL - ,[Diverse Adressnummer] = [dbo].[ott_quote_255]('') - ,[Geschäftspartnerbank] = null - ,[Sachverhalt] = null - ,[Zinssperre] = null - ,[Beleglink] = [dbo].[ott_quote_255](CASE WHEN [file_guid] is null THEN NULL - ELSE CAST( 'BEDI "' + LOWER(CAST([file_guid] as varchar(36))) + '"' as varchar(50)) - end) - --,[order] = ROW_NUMBER() OVER (ORDER BY iv.[DateOfCreation] ASC)] - ,[Beleginfo - Art 1] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 1] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Art 2] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 2] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Art 3] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 3] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Art 4] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 4] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Art 5] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 5] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Art 6] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 6] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Art 7] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 7] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Art 8] = [dbo].[ott_quote_255]('') - ,[Beleginfo - Inhalt 8] = [dbo].[ott_quote_255]('') - ,[KOST1 - Kostenstelle] = [dbo].[ott_quote_255]('') - ,[KOST2 - Kostenstelle] = [dbo].[ott_quote_255]('') - ,[Kost-Menge] = null - ,[EU-Land u. UStID (Bestimmung)] = [dbo].[ott_quote_255]('') - ,[EU-Steuersatz (Bestimmung)] = null - ,[Abw. Versteuerungsart] = [dbo].[ott_quote_255]('') - ,[Sachverhalt L+L] = null - ,[Funktionsergänzung L+L] = null - ,[BU 49 Hauptfunktionstyp] = null - ,[BU 49 Hauptfunktionsnummer] = null - ,[BU 49 Funktionsergänzung] = null - ,[Zusatzinformation - Art 1] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 1] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 2] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 2] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 3] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 3] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 4] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 4] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 5] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 5] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 6] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 6] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 7] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 7] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 8] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 8] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 9] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 9] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 10] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 10] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 11] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 11] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 12] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 12] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 13] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 13] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 14] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 14] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 15] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 15] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 16] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 16] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 17] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 17] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 18] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 18] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 19] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 19] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation - Art 20] = [dbo].[ott_quote_255]('') - ,[Zusatzinformation- Inhalt 20] = [dbo].[ott_quote_255]('') - ,[Stück] = null - ,[Gewicht] = null - ,[Zahlweise] = null - ,[Forderungsart] = [dbo].[ott_quote_255]('') - ,[Veranlagungsjahr] = null - ,[Zugeordnete Fälligkeit] = null - ,[Skontotyp] = null - ,[Auftragsnummer] = [dbo].[ott_quote_255]('') - ,[Buchungstyp] = [dbo].[ott_quote_255]('') - ,[USt-Schlüssel (Anzahlungen)] = null - ,[EU-Land (Anzahlungen)] = [dbo].[ott_quote_255]('') - ,[Sachverhalt L+L (Anzahlungen)] = null - ,[EU-Steuersatz (Anzahlungen)] = null - ,[Erlöskonto (Anzahlungen)] = null - ,[Herkunft-Kz] = [dbo].[ott_quote_255]('') - ,[Buchungs GUID] = [dbo].[ott_quote_255]('') - ,[KOST-Datum] = null - ,[Mandatsreferenz] = [dbo].[ott_quote_255]('') - ,[Skontosperre] = null - ,[Gesellschaftername] = [dbo].[ott_quote_255]('') - ,[Beteiligtennummer] = null - ,[Identifikationsnummer] = [dbo].[ott_quote_255]('') - ,[Zeichnernummer] = [dbo].[ott_quote_255]('') - ,[Postensperre bis] = null - ,[Bezeichnung SoBil-Sachverhalt] = [dbo].[ott_quote_255]('') - ,[Kennzeichen SoBil-Buchung] = [dbo].[ott_quote_255]('') - ,[Festschreibung] = 1 - ,[Leistungsdatum] = null - ,[Datum Zuord. Steuerperiode] = null - ,[Fälligkeit] = null - ,[Generalumkehr (GU)] = 0 - ,[Steuersatz] = null - ,[Land] = null - --,[Abrechnungsreferenz] - --,[BVV-Position] - --,[EU-Land u. UStID (Ursprung)] - --,[EU-Steuersatz (Ursprung)] - from inv as iv - order by TRY_CONVERT(integer, SUBSTRING(iv.[InvoiceId], 2,4)), TRY_CONVERT(integer, RIGHT(iv.[InvoiceId], 4)), iv.[DateOfCreation] ASC; - - - - - WIth debs as ( - SELECT ROW_NUMBER() OVER (PARTITION BY [customer.Id] ORDER BY SReq.DateOfCreation ASC) as [#] - , [customer.ExternalId] - , [customer.Id] - FROM @SReq as sreq - ), dd as ( - SELECT distinct cy.* FROM [mfr__companies] as cy WHERE EXISTS ( SELECT * FROM debs WHERE debs.[customer.Id] = cy.[id] AND debs.[#] = 1) - ), cc as ( - SELECT ROW_NUMBER() OVER (PARTITION BY dd.[id] ORDER BY CASE WHEN dd.[MainContactId] = c.[id] THEN 0 ELSE 1 END) as '#' - , c.* - , [company_id] = dd.[id] - , [company_name] = dd.[Name] - , [isCompany] = CAST ( - CASE WHEN ( not dd.[name] like '%c[/]o %' - AND ( - dd.[Name] like '%mbH%' - OR dd.[Name] like '%Ltd%' - OR dd.[Name] like '% gbr%' OR dd.[Name] like 'gbr %' - OR dd.[Name] like '% OHG%' - OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% AG %' - OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% KG %' - OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% SA %' - OR (REPLACE(dd.[Name], '|' , ' ') + ' ') like '% UG %' - OR dd.[Name] like '% eV%' or dd.[Name] like '%e[.]V[.]%' - OR dd.[Name] like '%Gesellschaft%' - OR dd.[Name] like '%Gemeinde%' - OR dd.[Name] like '%Verwaltung%' - OR dd.[Name] like '%Stadtwerke%' - OR dd.[Name] like '%tagesstätte%' - OR dd.[Name] like '%schützenverein%' - OR dd.[Name] like '%akademie%' - OR dd.[Name] like '%universität%' - OR dd.[Name] like '%schule%' - OR dd.[Name] like '%spedition%' - OR dd.[Name] like '%immobilien%' - OR dd.[Name] like '%krankenkasse%' - OR dd.[Name] like '% holding %' - OR dd.[Name] like '% Bautenschutz %' - OR dd.[Name] like '%dienstleistung%' - OR dd.[Name] like '%weinlokal%' - OR dd.[Name] like '%restaurant%' - OR dd.[Name] like '% hotel%' - OR dd.[Name] like '%kanzlei%' - OR dd.[Name] like '%praxis%' - OR dd.[Name] like '%großhandel%' - OR dd.[Name] like '%tischlerei%' - OR dd.[Name] like '%haustechnik%' - OR dd.[Name] like '%holztechnik%' - OR dd.[Name] like '%heizungsbau%' - OR dd.[Name] like '%bautenschutz%' - OR dd.[Name] like '%tischlerei%' - OR dd.[Name] like '%architekt%' - OR dd.[Name] like '%sanitär%' - OR dd.[Name] like '%pafümerie%' - OR dd.[Name] like '%bauamt%' - OR dd.[Name] like '%gebäudereinigung%' - OR dd.[Name] like '%handwerksmeister%' - OR dd.[Name] like '%fachbetrieb%' - OR dd.[Name] like '%ausbau und montagen%' - OR dd.[Name] like '%verband %' - OR dd.[Name] like '%vermietung%' - OR dd.[Name] like '%Bad [&] Heizung%' - OR dd.[Name] like '%Bad[&]Wärme%' - OR dd.[Name] like '% Düsseldorf%' - OR dd.[Name] like '%niederlassung%' - OR dd.[Name] like 'zentrum für %' - OR dd.[Name] like '% Generalvertretung %' - )) - - THEN 1 ELSE 0 END - as bit) - FROM [dbo].[mfr__contacts] as c JOIN dd on c.[CompanyId] = dd.[Id] OR dd.[MainContactId] = c.[id] - ) - SELECT - [Konto] = CAST(dd.[ExternalId] as bigint) --CAST(dd.[ExternalId] as varchar(9)) - ,[Name (Adressatentyp Unternehmen)] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN cc.[isCompany] = 0 THEN '' ELSE REPLACE(REPLACE(REPLACE(ISNULL( cc.[company_name] ,''), CHAR(13), ' '), CHAR(10), ' '), ' ', ' ') END as varchar(255)), 50) as varchar(50))) - ,[Unternehmensgegenstand] = [dbo].[ott_quote_255]('') - ,[Name (Adressattyp natürl. Person)] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN cc.[isCompany] = 1 THEN '' ELSE REPLACE(REPLACE(REPLACE(ISNULL( cc.[LastName] ,''), CHAR(13), ' '), CHAR(10), ' '), ' ', ' ') END as varchar(255)), 30) as varchar(30))) - ,[Vorname (Adressattyp natürl. Person)] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN cc.[isCompany] = 1 THEN '' ELSE REPLACE(REPLACE(REPLACE(ISNULL( cc.[FirstName] ,''), CHAR(13), ' '), CHAR(10), ' '), ' ', ' ') END as varchar(255)), 30) as varchar(30))) - ,[Name (Adressattyp keine Angabe)] = [dbo].[ott_quote_255]('') - ,[Adressattyp] = [dbo].[ott_quote_255](CAST(CASE WHEN cc.[isCompany] = 1 THEN 2 ELSE 1 END as varchar(1))) - ,[Kurzbezeichnung] = [dbo].[ott_quote_255]('') - ,[EU-Land] = [dbo].[ott_quote_255]('') - ,[EU-UStID] = [dbo].[ott_quote_255]('') - ,[Anrede] = [dbo].[ott_quote_255]('') --CASE WHEN LOWER(LEFT(cc.[Gender],1)) = 'm' THEN 'Herrn' WHEN LOWER(LEFT(cc.[Gender],1)) = 'w' THEN 'Frau' ELSE '' END - ,[Titel/Akad. Grad] = [dbo].[ott_quote_255]('') - ,[Adelstitel] = [dbo].[ott_quote_255]('') - ,[Namensvorsatz] = [dbo].[ott_quote_255]('') - ,[Adressart] = [dbo].[ott_quote_255](CAST( CASE WHEN ISNULL(l.[AddressString],'') like '%postfach%' THEN 'PF' ELSE 'STR' END as varchar(3))) - ,[Straße] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN ISNULL(l.[AddressString],'') like '%postfach%' THEN '' ELSE ISNULL(l.[AddressString],'') END as varchar(255)),36) as varchar(36))) - ,[Postfach] = [dbo].[ott_quote_255](CAST(LEFT(CAST(CASE WHEN ISNULL(l.[AddressString],'') like '%postfach%' THEN TRIM(SUBSTRING(ISNULL(l.[AddressString],''), PATINDEX('%postfach%', ISNULL(l.[AddressString],'')) + 8 , 12)) ELSE '' END as varchar(255)),36) as varchar(36))) - ,[Postleitzahl] = [dbo].[ott_quote_255](CAST(l.[Postal] as varchar(10))) - ,[Ort] = [dbo].[ott_quote_255](CAST(l.City as varchar(30))) - ,[Land] = [dbo].[ott_quote_255](CAST(REPLACE(UPPER(ISNULL(l.[Country], '')),'KE','') as varchar(2))) - ,[Versandzusatz] = [dbo].[ott_quote_255]('') - ,[Adresszusatz] = [dbo].[ott_quote_255]('') - ,[Abweichende Anrede] = [dbo].[ott_quote_255]('') - ,[Abw. Zustellbezeichnung 1] = [dbo].[ott_quote_255]('') - ,[Abw. Zustellbezeichnung 2] = [dbo].[ott_quote_255]('') - ,[Kennz. Korrespondenzadresse] = Cast(1 as int) - ,[Adresse Gültig von] = [dbo].[ott_quote_255]('') - ,[Adresse Gültig bis] = [dbo].[ott_quote_255]('') - ,[Telefon] = [dbo].[ott_quote_255](CAST(ISNULL(cc.[Telephone], dd.[SupportTelephone]) as varchar(60))) - ,[Bemerkung (Telefon)] = [dbo].[ott_quote_255]('') - ,[Telefon GL] = [dbo].[ott_quote_255]('') - ,[Bemerkung (Telefon GL)] = [dbo].[ott_quote_255]('') - ,[E-Mail] = [dbo].[ott_quote_255](CAST( CASE WHEN ISNULL(cc.[Email], dd.[SupportMail]) like '%[@]%' THEN REPLACE(LOWER(ISNULL(cc.[Email], dd.[SupportMail])), ' ', '') ELSE NULL END as varchar(60))) - ,[Bemerkung (E-Mail)] = [dbo].[ott_quote_255]('') - ,[Internet] = [dbo].[ott_quote_255]('') - ,[Bemerkung (Internet)] = [dbo].[ott_quote_255]('') - ,[Fax] = [dbo].[ott_quote_255]('') - ,[Bemerkung (Fax)] = [dbo].[ott_quote_255]('') - ,[Sonstige] = [dbo].[ott_quote_255]('') - ,[Bemerkung (Sonstige)] = [dbo].[ott_quote_255]('') - ,[Bankleitzahl 1] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 1] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 1] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 1] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 1] = [dbo].[ott_quote_255]('') - ,[IBAN1 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 1] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 1] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 1] = [dbo].[ott_quote_255]('') - ,[Bankverb 1 Gültig von] = null - ,[Bankverb 1 Gültig bis] = null - ,[Bankleitzahl 2] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 2] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 2] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 2] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 2] = [dbo].[ott_quote_255]('') - ,[IBAN2 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 2] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 2] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 2] = [dbo].[ott_quote_255]('') - ,[Bankverb 2 Gültig von] = null - ,[Bankverb 2 Gltig bis] = null - ,[Bankleitzahl 3] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 3] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 3] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 3] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 3] = [dbo].[ott_quote_255]('') - ,[IBAN3 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 3] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 3] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 3] = [dbo].[ott_quote_255]('') - ,[Bankverb 3 Gültig von] = null - ,[Bankverb 3 Gültig bis] = null - ,[Bankleitzahl 4] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 4] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 4] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 4] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 4] = [dbo].[ott_quote_255]('') - ,[IBAN4 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 4] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 4] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 4] = [dbo].[ott_quote_255]('') - ,[Bankverb 4 Gültig von] = null - ,[Bankverb 4 Gültig bis] = null - ,[Bankleitzahl 5] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 5] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 5] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 5] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 5] = [dbo].[ott_quote_255]('') - ,[IBAN5 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 5] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 5] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 5] = [dbo].[ott_quote_255]('') - ,[Bankverb 5 Gültig von] = null - ,[Bankverb 5 Gültig bis] = null - ,[Leerfeld] = null - ,[Briefanrede] = [dbo].[ott_quote_255]('') - ,[Grußformel] = [dbo].[ott_quote_255]('') - ,[Kundennummer] = [dbo].[ott_quote_255]('') - ,[Steuernummer] = [dbo].[ott_quote_255]('') - ,[Sprache] = null - ,[Ansprechpartner] = [dbo].[ott_quote_255]('') - ,[Vertreter] = [dbo].[ott_quote_255]('') - ,[Sachbearbeiter] = [dbo].[ott_quote_255]('') - ,[Diverse-Konto] = null - ,[Ausgabeziel] = null - ,[Währungssteuerung] = [dbo].[ott_quote_255]('') - ,[Kreditlimit (Debitor)] = null - ,[Zahlungsbedingung] = null - ,[Fälligkeit in Tagen (Debitor)] = null - ,[Skonto in Prozent (Debitor)] = null - ,[Kreditoren-Ziel 1 Tg.] = null - ,[Kreditoren-Skonto 1 %] = null - ,[Kreditoren-Ziel 2 Tg.] = null - ,[Kreditoren-Skonto 2 %] = null - ,[Kreditoren-Ziel 3 Brutto Tg.] = null - ,[Kreditoren-Ziel 4 Tg.] = null - ,[Kreditoren-Skonto 4 %] = null - ,[Kreditoren-Ziel 5 Tg.] = null - ,[Kreditoren-Skonto 5 %] = null - ,[Mahnung] = null - ,[Kontoauszug] = null - ,[Mahntext 1] = null - ,[Mahntext 2] = null - ,[Mahntext 3] = null - ,[Kontoauszugstext] = null - ,[Mahnlimit Betrag] = null - ,[Mahnlimit %] = null - ,[Zinsberechnung] = null - ,[Mahnzinssatz 1] = null - ,[Mahnzinssatz 2] = null - ,[Mahnzinssatz 3] = null - ,[Lastschrift] = [dbo].[ott_quote_255]('') - ,[Verfahren] = [dbo].[ott_quote_255]('') - ,[Mandantenbank] = null - ,[Zahlungsträger] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 1] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 2] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 3] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 4] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 5] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 6] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 7] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 8] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 9] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 10] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 11] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 12] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 13] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 14] = [dbo].[ott_quote_255]('') - ,[Indiv. Feld 15] = [dbo].[ott_quote_255]('') - ,[Abweichende Anrede (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Adressart (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Straße (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Postfach (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Postleitzahl (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Ort (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Land (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Versandzusatz (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Adresszusatz (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Abw. Zustellbezeichnung 1 (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Abw. Zustellbezeichnung 2 (Rechnungsadresse)] = [dbo].[ott_quote_255]('') - ,[Adresse Gültig von (Rechnungsadresse)] = null - ,[Adresse Gültig bis (Rechnungsadresse)] = null - ,[Bankleitzahl 6] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 6] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 6] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 6] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 6] = [dbo].[ott_quote_255]('') - ,[IBAN6 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 6] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 6] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 6] = [dbo].[ott_quote_255]('') - ,[Bankverb 6 Gültig von] = null - ,[Bankverb 6 Gültig bis] = null - ,[Bankleitzahl 7] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 7] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 7] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 7] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 7] = [dbo].[ott_quote_255]('') - ,[IBAN7 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 7] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 7] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 7] = [dbo].[ott_quote_255]('') - ,[Bankverb 7 Gültig von] = null - ,[Bankverb 7 Gültig bis] = null - ,[Bankleitzahl 8] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 8] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 8] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 8] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 8] = [dbo].[ott_quote_255]('') - ,[IBAN8 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 8] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 8] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 8] = [dbo].[ott_quote_255]('') - ,[Bankverb 8 Gültig von] = null - ,[Bankverb 8 Gültig bis] = null - ,[Bankleitzahl 9] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 9] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 9] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 9] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 9] = [dbo].[ott_quote_255]('') - ,[IBAN9 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 9] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 9] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 9] = [dbo].[ott_quote_255]('') - ,[Bankverb 9 Gültig von] = null - ,[Bankverb 9 Gültig bis] = null - ,[Bankleitzahl 10] = [dbo].[ott_quote_255]('') - ,[Bankbezeichnung 10] = [dbo].[ott_quote_255]('') - ,[Bank-Kontonummer 10] = [dbo].[ott_quote_255]('') - ,[Länderkennzeichen 10] = [dbo].[ott_quote_255]('') - ,[IBAN-Nr. 10] = [dbo].[ott_quote_255]('') - ,[IBAN10 korrekt] = [dbo].[ott_quote_255]('') - ,[SWIFT-Code 10] = [dbo].[ott_quote_255]('') - ,[Abw. Kontoinhaber 10] = [dbo].[ott_quote_255]('') - ,[Kennz. Hauptbankverb. 10] = [dbo].[ott_quote_255]('') - ,[Bankverb 10 Gültig von] = null - ,[Bankverb 10 Gültig bis] = null - ,[Nummer Fremdsystem] = [dbo].[ott_quote_255]('') - ,[Insolvent] = null - ,[Mandatsreferenz 1] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 2] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 3] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 4] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 5] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 6] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 7] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 8] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 9] = [dbo].[ott_quote_255]('') - ,[Mandatsreferenz 10] = [dbo].[ott_quote_255]('') - ,[Verknüpftes OPOS-Konto] = null - ,[Mahnsperre bis] = null - ,[Lastschriftsperre bis] = null - ,[Zahlungssperre bis] = null - ,[gebührenberechnung] = null - ,[Mahngebühr 1] = null - ,[Mahngebühr 2] = null - ,[Mahngebühr 3] = null - ,[Pauschalenberechnung] = null - ,[Verzugspauschale 1] = null - ,[Verzugspauschale 2] = null - ,[Verzugspauschale 3] = null - ,[Alternativer Suchname] = null - ,[Status] = null - ,[Anschrift manuell geändert (Korrespondenzadresse)] = null - ,[Anschrift individuell (Korrespondenzadresse)] = null - ,[Anschrift manuell geändert (Rechnungsadresse)] = null - ,[Anschrift individuell (Rechnungsadresse)] = null - ,[Fristberechnung bei Debitor] = null - ,[Mahnfrist 1] = null - ,[Mahnfrist 2] = null - ,[Mahnfrist 3] = null - ,[Letzte Frist] = null - FROM dd JOIN cc on cc.[#] = 1 AND cc.[company_id] = dd.[id] - LEFT JOIN [dbo].[mfr__#locations] as l ON l.[EntityID] = dd.[id] and l.Property = 'Company:Location' -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvRequestItems.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvRequestItems.sql deleted file mode 100644 index e5bd618..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvRequestItems.sql +++ /dev/null @@ -1,57 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getInvRequestItems] - @invoiceid varchar(20) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @idbigint bigint = TRY_CAST(@invoiceid as bigint); - - - DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(20), [customer.ExternalId] bigint, [customer.Id] bigint, [mfr] bit); - - INSERT INTO @SReq - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = TRY_CAST(i.[Id] as varchar(20)), [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], [mfr] = 1 - FROM [dbo].[mfr__servicerequests] as s JOIN [mfr__reports] as i on i.[id] = @idbigint and s.[InvoiceId] like ('%' + i.[InvoiceId] + '%') LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - where i.[id] = @idbigint - UNION - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], [mfr] = 0 - FROM [dbo].[mfr__servicerequests] as s - JOIN [dbo].[fds__invoice_servicerequests] as iq ON s.[id] = iq.[mfr__servicerequest] - JOIN [dbo].[fds__invoices] as i on iq.[InvId] = i.[id] AND i.[id] = @invoiceid - LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - where i.[id] = @invoiceid - ; - - SELECT - [id],[DateOfCreation],[name],[invoice.Id],[customer.ExternalId],[customer.Id], [mfr] - ,[order] = ROW_NUMBER() OVER (ORDER BY [DateOfCreation]) - FROM @SReq; - - - SELECT - itm.[Id] - , [net_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2)) , '#0.00 €','de') END - , [bo_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) , '#0.00 €','de') END - , [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END - , itm.[ServiceRequestId] - , itm.[SortOrder] - , itm.[Type] - , itm.[NameOrNumber] - , sreq.[invoice.Id] - , [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder]) - FROM [dbo].[mfr__items] as itm - JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item' - ; - - - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoiceReminder.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoiceReminder.sql deleted file mode 100644 index f5eaad5..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoiceReminder.sql +++ /dev/null @@ -1,43 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getInvoiceReminder] - @InvId varchar(15) - , @include_drafts bit = 0 - ,@authuser varchar(25) -AS -BEGIN - SET NOCOUNT ON; - - With inv as ( - SELECT TOP(1) - [Id] = CAST([id] as varchar(15)), [InvoiceId], [DocumentName] - FROM [dbo].[fds__invoices] - WHERE [id] = @InvId - ), inv2 as( - SELECT * FROM inv - UNION - SELECT TOP(1) - [Id] = CAST([id] as varchar(15)), [InvoiceId], [DocumentName] - FROM [dbo].[mfr__invoices] as mi - WHERE NOT EXISTS( SELECT 0 FROM inv ) - AND TRY_CAST(@InvId as bigint) is not null - AND mi.[id] = TRY_CAST(@InvId as bigint) - ) - SELECT rem.[Id] - ,rem.[DateSent] - ,[status] = CAST(CASE WHEN rem.[isSent] = 1 THEN 'versandt' WHEN rem.[isfinal] = 1 THEN 'fertig' WHEN rem.isfinal = 0 THEN 'Entwurf' ELSE '' END AS varchar(50)) - ,rem.[subject] - ,rem.[DocumentName] - ,rem.[amount_open] - ,rem.[InvId] - ,[InvoiceId] = inv2.[InvoiceId] - --,[InvoiceDocumentName] = inv2.[DocumentName] - ,[hasFile] = cast( IIF(rem.[file] is null, 0, 1) as bit) - FROM [dbo].[fds__reminder] as rem - LEFT JOIN inv2 on rem.[InvId] = inv2.[Id] - WHERE [invId] = @InvId; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoices_list2.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoices_list2.sql deleted file mode 100644 index a601a60..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoices_list2.sql +++ /dev/null @@ -1,285 +0,0 @@ - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getInvoices_list2] - @tgtdate date - ,@mode varchar(1) = 'm' - , @include_drafts bit = 0 - , @search varchar(100) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__getInvoices_list' ,@authuser , @search); - - DECLARE @startdate date, @enddate date, @today date = GETDATE(); - DECLARE @true bit = 1, @false bit = 0; - DECLARE @searchmode as varchar(1) = CASE WHEN SUBSTRING(ISNULL(@search, '') + ' ', 2,1) <> ':' THEN '' ELSE LOWER(LEFT(@search,1 )) END; - - - DECLARE @customerfilter TABLE ([CustomerID] bigint); - - IF @searchmode <> '' - BEGIN - SELECT @mode = 's', @search = SUBSTRING(@search,3, 1000); - - IF @searchmode = 'c' AND LEN(@search) > 3 - BeGIN - INSERT INTO @customerfilter SELECT [id] FROM [dbo].[mfr__companies] WHERE [Name] like ('%' + @search + '%'); - END - END - ELSE - SET @mode = LOWER(@mode); - If @mode = 'm' - BEGIN - SELECT @startdate = [dbo].[date_monthfirst](@tgtdate) - ,@enddate = [dbo].[date_monthend](@tgtdate); - END - ELSE If @mode = 'w' - BEGIN - SELECT @startdate = [dbo].[date_weekfirst](@tgtdate) - ,@enddate = [dbo].[date_weekend](@tgtdate); - END; - - - --Reminder settings - DECLARE @stage1 varchar(10), @stage2 varchar(10), @stage3 varchar(10); - SELECT @stage1 = [stage1], @stage2 = [stage2], @stage3 = [stage3] FROM [dbo].[fds__admin_reminderSettings](); - - - - SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode - , [title] = 'Rechnungsübersicht für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy') - WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.') - WHEN @searchmode = 'i' THEN 'Suche nach Rechnungsnummer ''' + @search + '''' - WHEN @searchmode = 'c' THEN 'Suche nach Kunde ''' + @search + '''' - ELSE '' END - ,[ust_options] = '19,0%;16,0%;0,0%'; - - - DECLARE @inv TABLE([Id] varchar(15), [InvoiceID] varchar(255), [URI] nvarchar(255), [DocumentName] nvarchar(255), [DateOfCreation] date, [fds] bit); - --INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__reports] where [IsInvoice] = 1 AND CAST([DateOfCreation] as date) between @startdate AND @enddate; - INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__invoices] - where (CASE WHEN @searchmode = 'i' THEN - (CASE WHEN [InvoiceID] = @search OR [InvoiceID] like ('R20[0-9][0-9][-]' + @search) THEN 1 ELSE 0 END) - ELSE - (CASE WHEN CAST([DateOfCreation] as date) between @startdate AND @enddate and (ISNULL(@include_drafts,0) = 1 OR [FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview','')) THEN 1 ELSE 0 END) - END) = 1; - INSERT INTO @inv SELECT [Id],[invoiceid],[URI] = null,[DocumentName],[DateOfCreation] = [DateCreated],[fds] = 1 FROM [dbo].[fds__invoices] - where (CASE WHEN @searchmode = 'i' THEN - (CASE WHEN [InvoiceID] = @search OR [InvoiceID] like ('R20[0-9][0-9][-]' + @search) THEN 1 ELSE 0 END) - WHEN @searchmode = 'c' THEN - (CASE WHEN [CustomerId] IN (SELECT [CustomerId] fROM @customerfilter) THEN 1 ELSE 0 END) - ELSE - (CASE WHEN CAST([DateCreated] as date) between @startdate AND @enddate and (ISNULL(@include_drafts,0) = 1 OR [isfinal] = 1) THEN 1 ELSE 0 END) - END) = 1; - - - DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(15), [customer.ExternalId] bigint, [customer.Id] bigint, [ExternalId] nvarchar(255), [fds] bit); - INSERT INTO @SReq - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds] - FROM [dbo].[mfr__servicerequests] as s - JOIN dbo.[mfr__*PartnerSet] as p on p.[EntityId] = s.[Id] AND p.[Property] = 'ServiceRequest:Invoices' - JOIN @inv as i on i.fds = 0 and p.[PartnerId] = i.[id] --s.[InvoiceId] like ('%' + i.[InvoiceId] + '%') - LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - WHERE i.fds = 0; - INSERT INTO @SReq - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds] - FROM [dbo].[fds__invoice_servicerequests] as irq - JOIN @inv as i on irq.[InvId] = i.[Id] - LEFT JOIN [dbo].[mfr__servicerequests] as s ON irq.[mfr__servicerequest] = s.[id] - LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - WHERE i.fds = 1; - - With itm as ( - SELECT - [net_itemsum] = CAST( SUM((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) as numeric(12,2)) - , [bo_itemsum] = CAST( SUM( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01))) as numeric(12,2)) - , [vf] = MAX(1 + (ISNULL(vat.[vat], 19.0) * 0.01)) - , sreq.[invoice.Id] - ,[fds] = 0 - FROM [dbo].[mfr__items] as itm - JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item' - WHERE [fds] = 0 - GROUP BY sreq.[invoice.Id] - UNION - SELECT - [net_itemsum] = CAST( SUM(ISNULL([value_total],0)) as numeric(12,2)) - , [bo_itemsum] = CAST( SUM(ISNULL([value_total],0) * (1 + (ISNULL([vat], 19.0) * 0.01))) as numeric(12,2)) - , [vf] = MAX(1 + (ISNULL([vat], 19.0) * 0.01)) - , [invoice.Id] = inv.[id] --sreq.[invoice.Id] - , [fds] = 1 - FROM [dbo].[fds__invoice_items] as itm - --JOIN @sreq as sreq ON itm.[InvRqId] = SReq.[id] - JOIN @inv as inv ON itm.[invid] = inv.[id] and [fds] = 1 - WHERE [fds] = 1 - GROUP BY inv.[id] --sreq.[invoice.Id] - ),sc as( - SELECT s.[invoice.Id], [fds], [#] = COUNT(s.[id]) FROM @SReq as s GROUP BY s.[invoice.Id], [fds] - ),contra_accounts as( - SELECT [account] = [key], [vat] = TRY_PARSE([value2] as numeric(5,2) USING 'en-US') FROM [dbo].[fds__admin_settings] WHERE [type] = 'contra_account' - ), inv as( - --select - -- [Id] = CAST(iv.[Id] as varchar(15)) - -- ,[InvoiceId] = iv.[InvoiceId] - -- ,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i' - -- WHEN LEFT([documentname],5) = 'storn' THEN 'c' - -- ELSE 'r' - -- END - -- as char(1)) - -- ,[DocumentName] - -- ,[requestcount] = sc.[#] - -- ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id) - -- ,[balance] = FORMAT(ABS([invoiceBalance]), '#0.00', 'de') + ' €' - -- ,[EINZELPOS_brutto] = FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €' - -- ,[EINZELPOS_netto] = FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €' - -- ,[vat] = FORMAT(ISNULL(vat.[vat], 19.0), '#0.0', 'de') + ' %' - -- ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - -- ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - -- ,[contra_account] = ca.[account] - -- ,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy') - -- ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) - -- ,iv.[DateOfCreation] - -- ,iv.[DueDate] - -- ,[PaymentTerm] = @stage1 - -- ,ivi.[PaymentStatus] - -- ,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL(ivi.[IsPayed],0) END as bit) - -- ,[IsCanceled] - -- ,[isSent] = @true - -- ,[isFinal] = @true - -- ,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit) - -- ,[fds] = @false - -- from [dbo].[mfr__reports] as iv - -- LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid] - -- LEFT JOIN itm on iv.Id = itm.[invoice.Id] - -- LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report' - -- LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0 - -- LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat] - -- where iv.Id in (SELECT [Id] FROM @inv Where fds = 0) - --UNION - select - [Id] = CAST(iv.[Id] as varchar(15)) - ,[InvoiceId] = iv.[InvoiceId] - ,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i' - WHEN LEFT([documentname],5) = 'storn' THEN 'c' - ELSE 'r' - END - as char(1)) - ,[DocumentName] - ,[requestcount] = sc.[#] - ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = TRY_CAST(iv.id as varchar(50))) - ,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €' - ,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €' - ,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €' - ,[vat] = FORMAT(ISNULL(vat.[vat], ISNULL([dbo].[fds__fn_vatfrombalances]([invoiceBalance], [invoiceBalancenetto]) ,19.0)), '#0.0', 'de') + ' %' - ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - --,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - ,[contra_account] = ca.[account] - ,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy') - ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id]) - ,iv.[DateOfCreation] - ,iv.[DueDate] - ,[PaymentTerm] = @stage1 - ,ivi.[PaymentStatus] - ,[IsPayed] = CAST(CASE WHEN ISNULL(ivi.isPayed,0) = 1 THEN 1 WHEN [InvoiceState] = 'eIsCancelled' THEN 1 WHEN [InvoiceState] = 'eIsPaid' THEN 1 ELSE 0 END as bit) - ,[IsCanceled] = CAST(CASE WHEN [InvoiceState] = 'eIsCancelled' THEN 1 ELSE 0 END as bit) - ,[isSent] = @true - ,[isFinal] = @true - ,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit) - ,[fds] = @false - ,[customer_id] = ps.[PartnerId] - from [dbo].[mfr__invoices] as iv - LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid] - LEFT JOIN itm on TRY_CAST(iv.id as varchar(50)) = itm.[invoice.Id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report' - LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0 - LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat] - LEFT JOIN [dbo].[mfr__*PartnerSet] as ps ON iv.[Id] = ps.[EntityId] AND ps.[Property] = 'Invoice:Customer' - where TRY_CAST(iv.id as varchar(50)) in (SELECT [Id] FROM @inv Where fds = 0) - UNION - select - iv.[Id] - ,[InvoiceId] = iv.[InvoiceId] + ISNULL((CASE WHEN iv.InvoiceType = 'c' THEN (SELECT TOP(1) ' >> ' + i2.[InvoiceId] FROM [dbo].[fds__invoices] as i2 JOIN [dbo].[fds__invoice_details] as id2 on i2.[Id] = id2.[StornoTo_InvId] where id2.[InvId] = iv.[id] and i2.[IsFinal] = 1) ELSE '' END), '') - ,[InvoiceType] - ,[DocumentName] - ,[requestcount] = sc.[#] - ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id) - ,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €' - ,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €' - ,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €' - ,[vat] = FORMAT(ISNULL(iv.[InvoiceVAT_1], 19.0), '#0.0', 'de') + ' %' - ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - --,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - ,[contra_account] = ca.[account] - ,[Belegdatum] = FORMAT(iv.[DateCreated], 'dd.MM.yy') - ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) - ,[DateOfCreation] = iv.[DateCreated] - ,[DueDate] - ,[PaymentTerm] = ISNULL([PaymentTerm], @stage1) - ,[PaymentStatus] - ,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL([IsPayed],0) END as bit) - ,[IsCanceled] - ,[isSent] - ,[isFinal] - ,[hasFile] = CAST( CASE wHEN [file] is not null and [DocumentNAme] is not null THEN 1 ELSE 0 END as bit) - ,[fds] = @true - ,[customer_id] = iv.[CustomerId] - from [dbo].[fds__invoices] as iv - LEFT JOIN itm on iv.Id = itm.[invoice.Id] - LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 1 - LEFT JOIN contra_accounts as ca ON ISNULL(iv.[InvoiceVAT_1], 19.0) = ca.[vat] - where iv.Id in (SELECT [Id] FROM @inv Where fds = 1) - ), reminder as ( - SELECT [InvId], [lastReminder] = MAX([DateFinalized]), [#] = COUNT(*) FROM [dbo].[fds__reminder] WHERE [IsFinal] = 1 AND [invId] IN (SELECT [Id] FROM @inv) GROUP BY [InvId] - ) - select - inv.* - ,[reminderstatus] = CASE WHEN [ispayed] = 1 THEN 0 - WHEN [dbo].[fds__fn_isInvoiceCancelled](inv.[Id]) = 1 THEN 0 - WHEN DueDate is null THEN 0 - WHEN [deb_cred] = 'H' THEN 0 - WHEN @stage3 <> '' AND rem.[#] = 2 AND @today > [dbo].[date_addcustom]([lastReminder], @stage3) THEN 3 - WHEN @stage2 <> '' AND rem.[#] = 1 AND @today > [dbo].[date_addcustom]([lastReminder], @stage2) THEN 2 - WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), ISNULL([PaymentTerm],@stage1)) THEN 1 - ELSE 0 END - ,[invstatus] = CASE WHEN [IsPayed] = 1 AND [isCanceled] = 0 THEN 'pyd' - WHEN [isCanceled] = 1 OR [dbo].[fds__fn_isInvoiceCancelled](inv.[Id]) = 1 THEN 'cc' - WHEN [isFinal] = 0 THEN 'dft' - WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns' - WHEN [DueDate] is null THEN 'op' - WHEN [deb_cred] = 'H' THEN '' - WHEN @today >= [DueDate] THEN 'due' - WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), @stage1) THEN 'ovd' - WHEN ISNULL(rem.[#],0) > 0 THEN 'rem' - END - ,[reminder] = rem.[#] - ,[CustomerName] = c.[name] - ,[order] = CASE WHEN @include_drafts = 1 THEN - ROW_NUMBER() OVER (ORDER BY inv.[DateOfCreation] ASC) - ELSE - ROW_NUMBER() OVER (ORDER BY TRY_CAST(SUBSTRING([invoiceid], 2,4) AS int), TRY_CAST(SUBSTRING([invoiceid], 7,4) AS int), inv.[DateOfCreation] ASC) - END - ,[Payment] = CASE WHEN [paymentstatus] = 'a' THEN 'vollst.' - WHEN [paymentstatus] = 'p' THEN FORMAT(ISNULL([dbo].[fds__fn_InvoicePaymentAmount](inv.[Id]),0.0), '0.00€', 'de') - ELSE '' END - FROM inv - left JOIN reminder as rem ON inv.[Id] = rem.[InvId] --inv.[id] is already converted to varchar - LEFT JOIN [dbo].[mfr__companies] as c ON inv.[customer_id] = c.[id] - WHERE (CASE WHEN @searchmode = 'c' THEN - (CASE WHEN inv.[customer_id] IN (SELECT [CustomerId] fROM @customerfilter) THEN 1 ELSE 0 END) - ELSE 1 END) = 1 - order by inv.[DateOfCreation] ASC; - - - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoices_list_vario.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoices_list_vario.sql deleted file mode 100644 index 00302ac..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getInvoices_list_vario.sql +++ /dev/null @@ -1,293 +0,0 @@ - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getInvoices_list_vario] - @tgtdate date - ,@mode varchar(1) = 'm' - , @includes varchar(50) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @include_drafts bit = [dbo].[ott_csv_contains](@includes, 'd'); - DECLARE @only_drafts bit = 0, @only_unpayed bit = 0, @only_reminded bit = 0, @only_akonto bit = 0, @only_storno bit = 0, @only_remindedunpayed bit = 0; - DECLARE @includetext varchar(50) = ''; - - IF @includes = '#d' - SELECT @includetext = ' (nur Entwürfe) ', @include_drafts = 1, @only_drafts = 1, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0; - ELSE IF @includes = 'all' - SELECT @includetext = ' (alle) ', @include_drafts = 1, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0; - ELSE IF @includes = '#r' - SELECT @includetext = ' (nur angemahnte) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 1, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0; - ELSE IF @includes = '#u' - SELECT @includetext = ' (nur unbezahlte) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 1, @only_akonto = 0, @only_storno = 0; - ELSE IF @includes = '#a' - SELECT @includetext = ' (nur Akonto) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 1, @only_storno = 0; - ELSE IF @includes = '#c' - SELECT @includetext = ' (nur Storno) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 1; - ELSE IF @includes = '#ru' - SELECT @includetext = ' (nur gemahnt + unbez. + nicht storniert) ', @include_drafts = 0, @only_drafts = 0, @only_reminded = 0, @only_unpayed = 0, @only_akonto = 0, @only_storno = 0, @only_remindedunpayed = 1; - - - IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__getInvoices_list' ,@authuser , ''); - - DECLARE @startdate date, @enddate date, @today date = GETDATE(); - DECLARE @true bit = 1, @false bit = 0; - SET @mode = LOWER(@mode); - If @mode = 'm' - BEGIN - SELECT @startdate = [dbo].[date_monthfirst](@tgtdate) - ,@enddate = [dbo].[date_monthend](@tgtdate); - END - ELSE If @mode = 'w' - BEGIN - SELECT @startdate = [dbo].[date_weekfirst](@tgtdate) - ,@enddate = [dbo].[date_weekend](@tgtdate); - END - ELSE If @mode = 'a' - BEGIN - SELECT @startdate = [dbo].[date_weekfirst]('2000-01-01') - ,@enddate = [dbo].[date_weekend](DATEADD(year, 1, @today)); - END - - --Reminder settings - DECLARE @stage1 varchar(10), @stage2 varchar(10), @stage3 varchar(10); - SELECT @stage1 = [stage1], @stage2 = [stage2], @stage3 = [stage3] FROM [dbo].[fds__admin_reminderSettings](); - - - - SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode - , [title] = 'Rechnungsübersicht ' + @includetext + ' für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy') - WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.') - WHEN @mode = 'a' THEN 'den gesamten Zeitraum' - ELSE '' END - ,[ust_options] = '19,0%;16,0%;0,0%'; - - - DECLARE @inv TABLE([Id] varchar(15), [InvoiceID] varchar(255), [URI] nvarchar(255), [DocumentName] nvarchar(255), [DateOfCreation] date, [fds] bit); - --INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__reports] where [IsInvoice] = 1 AND CAST([DateOfCreation] as date) between @startdate AND @enddate; - INSERT INTO @inv SELECT [Id],[invoiceid],[URI],[DocumentName],[DateOfCreation],[fds] = 0 FROM [dbo].[mfr__invoices] - where CAST([DateOfCreation] as date) between @startdate AND @enddate - and (ISNULL(@include_drafts,0) = 1 OR [FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview','')) - and (@only_drafts = 0 OR ISNULL(InvoiceId,'') in ('Preview','')) - and (@only_akonto = 0 OR [FileType] = 'PdfPartialInvoice') - and (@only_storno = 0 OR [FileType] = 'PdfCancelInvoice') - and ((@only_unpayed = 0 AND @only_remindedunpayed = 0) OR [InvoiceState] not IN ('eIsCancelled', 'eIsPaid')); - INSERT INTO @inv SELECT [Id],[invoiceid],[URI] = null,[DocumentName],[DateOfCreation] = [DateCreated],[fds] = 1 FROM [dbo].[fds__invoices] - where CAST([DateCreated] as date) between @startdate AND @enddate - and (ISNULL(@include_drafts,0) = 1 OR [isfinal] = 1) - and (@only_drafts = 0 OR [isfinal] = 0) - and (@only_akonto = 0 OR [InvoiceType] = 'i') - and (@only_storno = 0 OR [InvoiceType] = 'c') - and ((@only_unpayed = 0 AND @only_remindedunpayed = 0) OR (IsPayed = 0 AND IsCanceled = 0)); - - DECLARE @SReq TABLE ([id] bigint, [DateOfCreation] datetime, [name] nvarchar(255), [invoice.Id] varchar(15), [customer.ExternalId] bigint, [customer.Id] bigint, [ExternalId] nvarchar(255), [fds] bit); - INSERT INTO @SReq - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds] - FROM [dbo].[mfr__servicerequests] as s - JOIN dbo.[mfr__*PartnerSet] as p on p.[EntityId] = s.[Id] AND p.[Property] = 'ServiceRequest:Invoices' - JOIN @inv as i on i.fds = 0 and p.[PartnerId] = i.[id] --s.[InvoiceId] like ('%' + i.[InvoiceId] + '%') - LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - WHERE i.fds = 0; - INSERT INTO @SReq - SELECT s.[Id], s.[DateOfCreation], s.[name], [invoice.Id] = i.[Id], [customer.ExternalId] = cy.ExternalId , [customer.Id] = cy.[id], s.[ExternalId], [fds] - FROM [dbo].[fds__invoice_servicerequests] as irq - JOIN @inv as i on irq.[InvId] = i.[Id] - LEFT JOIN [dbo].[mfr__servicerequests] as s ON irq.[mfr__servicerequest] = s.[id] - LEFT JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - WHERE i.fds = 1; - - With itm as ( - SELECT - [net_itemsum] = CAST( SUM((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) as numeric(12,2)) - , [bo_itemsum] = CAST( SUM( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01))) as numeric(12,2)) - , [vf] = MAX(1 + (ISNULL(vat.[vat], 19.0) * 0.01)) - , sreq.[invoice.Id] - ,[fds] = 0 - FROM [dbo].[mfr__items] as itm - JOIN @sreq as sreq ON itm.[ServiceRequestId] = SReq.[id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item' - WHERE [fds] = 0 - GROUP BY sreq.[invoice.Id] - UNION - SELECT - [net_itemsum] = CAST( SUM(ISNULL([value_total],0)) as numeric(12,2)) - , [bo_itemsum] = CAST( SUM(ISNULL([value_total],0) * (1 + (ISNULL([vat], 19.0) * 0.01))) as numeric(12,2)) - , [vf] = MAX(1 + (ISNULL([vat], 19.0) * 0.01)) - , [invoice.Id] = inv.[id] --sreq.[invoice.Id] - , [fds] = 1 - FROM [dbo].[fds__invoice_items] as itm - --JOIN @sreq as sreq ON itm.[InvRqId] = SReq.[id] - JOIN @inv as inv ON itm.[invid] = inv.[id] and [fds] = 1 - WHERE [fds] = 1 - GROUP BY inv.[id] --sreq.[invoice.Id] - ),sc as( - SELECT s.[invoice.Id], [fds], [#] = COUNT(s.[id]) FROM @SReq as s GROUP BY s.[invoice.Id], [fds] - ),contra_accounts as( - SELECT [account] = [key], [vat] = TRY_PARSE([value2] as numeric(5,2) USING 'en-US') FROM [dbo].[fds__admin_settings] WHERE [type] = 'contra_account' - ), inv as( - --select - -- [Id] = CAST(iv.[Id] as varchar(15)) - -- ,[InvoiceId] = iv.[InvoiceId] - -- ,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i' - -- WHEN LEFT([documentname],5) = 'storn' THEN 'c' - -- ELSE 'r' - -- END - -- as char(1)) - -- ,[DocumentName] - -- ,[requestcount] = sc.[#] - -- ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id) - -- ,[balance] = FORMAT(ABS([invoiceBalance]), '#0.00', 'de') + ' €' - -- ,[EINZELPOS_brutto] = FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €' - -- ,[EINZELPOS_netto] = FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €' - -- ,[vat] = FORMAT(ISNULL(vat.[vat], 19.0), '#0.0', 'de') + ' %' - -- ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - -- ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - -- ,[contra_account] = ca.[account] - -- ,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy') - -- ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) - -- ,iv.[DateOfCreation] - -- ,iv.[DueDate] - -- ,[PaymentTerm] = @stage1 - -- ,ivi.[PaymentStatus] - -- ,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL(ivi.[IsPayed],0) END as bit) - -- ,[IsCanceled] - -- ,[isSent] = @true - -- ,[isFinal] = @true - -- ,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit) - -- ,[fds] = @false - -- from [dbo].[mfr__reports] as iv - -- LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid] - -- LEFT JOIN itm on iv.Id = itm.[invoice.Id] - -- LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report' - -- LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0 - -- LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat] - -- where iv.Id in (SELECT [Id] FROM @inv Where fds = 0) - --UNION - select - [Id] = CAST(iv.[Id] as varchar(15)) - ,[InvoiceId] = iv.[InvoiceId] - ,[InvoiceType] = CAST( CASE WHEN LEFT([documentname],5) = 'absch' THEN 'i' - WHEN LEFT([documentname],5) = 'storn' THEN 'c' - ELSE 'r' - END - as char(1)) - ,[DocumentName] - ,[requestcount] = sc.[#] - ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = TRY_CAST(iv.id as varchar(50))) - ,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €' - ,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €' - ,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €' - ,[vat] = FORMAT(ISNULL(vat.[vat], ISNULL([dbo].[fds__fn_vatfrombalances]([invoiceBalance], [invoiceBalancenetto]) ,19.0)), '#0.0', 'de') + ' %' - ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - ,[contra_account] = ca.[account] - ,[Belegdatum] = FORMAT(iv.[DateOfCreation], 'dd.MM.yy') - ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE TRY_CAST(iv.id as varchar(50)) = sreq.[invoice.Id]) - ,iv.[DateOfCreation] - ,iv.[DueDate] - ,[PaymentTerm] = @stage1 - ,ivi.[PaymentStatus] - ,[IsPayed] = CAST(CASE WHEN ISNULL(ivi.isPayed,0) = 1 THEN 1 WHEN [InvoiceState] = 'eIsCancelled' THEN 1 WHEN [InvoiceState] = 'eIsPaid' THEN 1 ELSE 0 END as bit) - ,[IsCanceled] = CAST(CASE WHEN [InvoiceState] = 'eIsCancelled' THEN 1 ELSE 0 END as bit) - ,[isSent] = @true - ,[isFinal] = @true - ,[hasFile] = CAST( CASE wHEN ISNULL([URI], '') <> '' THEN 1 ELSE 0 END as bit) - ,[fds] = @false - ,[customer_id] = ps.[PartnerId] - from [dbo].[mfr__invoices] as iv - LEFT JOIN [dbo].[fds__custom_invoiceinfo] as ivi on iv.[id] = ivi.[invid] - LEFT JOIN itm on TRY_CAST(iv.id as varchar(50)) = itm.[invoice.Id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON iv.[Id] = vat.[EntityId] and vat.[EntityType] = 'report' - LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 0 - LEFT JOIN contra_accounts as ca ON ISNULL(vat.[vat], 19.0) = ca.[vat] - LEFT JOIN [dbo].[mfr__*PartnerSet] as ps ON iv.[Id] = ps.[EntityId] AND ps.[Property] = 'Invoice:Customer' - where TRY_CAST(iv.id as varchar(50)) in (SELECT [Id] FROM @inv Where fds = 0) - UNION - select - iv.[Id] - ,[InvoiceId] = iv.[InvoiceId] + ISNULL((CASE WHEN iv.InvoiceType = 'c' THEN (SELECT TOP(1) ' >> ' + i2.[InvoiceId] FROM [dbo].[fds__invoices] as i2 JOIN [dbo].[fds__invoice_details] as id2 on i2.[Id] = id2.[StornoTo_InvId] where id2.[InvId] = iv.[id] and i2.[IsFinal] = 1) ELSE '' END), '') - ,[InvoiceType] - ,[DocumentName] - ,[requestcount] = sc.[#] - ,[request] = (SELECT TOP(1) s.[ExternalId] FROM @SReq as s WHERE s.[invoice.Id] = iv.id) - ,[balance] = FORMAT([invoiceBalance], '#0.00', 'de') + ' €' - ,[EINZELPOS_brutto] = NULL --FORMAT(itm.bo_itemsum, '#0.00', 'de') + ' €' - ,[EINZELPOS_netto] = NULL --FORMAT(itm.net_itemsum, '#0.00', 'de') + ' €' - ,[vat] = FORMAT(ISNULL(iv.[InvoiceVAT_1], 19.0), '#0.0', 'de') + ' %' - ,[deb_cred] = CASE WHEN [invoiceBalance] >= 0 THEN 'S' ELSE 'H' END - ,[customer] = (SELECT TOP(1) [customer.ExternalId] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id] and [customer.ExternalId] is not null ORDER BY SReq.DateOfCreation ASC) - ,[contra_account] = ca.[account] - ,[Belegdatum] = FORMAT(iv.[DateCreated], 'dd.MM.yy') - ,[Buchungstext] = (SELECT TOP(1) SReq.[name] FROM @SReq as sreq WHERE iv.[Id] = sreq.[invoice.Id]) - ,[DateOfCreation] = iv.[DateCreated] - ,[DueDate] - ,[PaymentTerm] = ISNULL([PaymentTerm], @stage1) - ,[PaymentStatus] - ,[IsPayed] = CAST(CASE WHEN ISNULL(iv.[IsCanceled],0) = 1 THEN 1 ELSE ISNULL([IsPayed],0) END as bit) - ,[IsCanceled] = CAST(IIF(ISNULL([IsCanceled],0) = 1 OR [dbo].[fds__fn_isInvoiceCancelled](iv.[Id]) = 1, 1,0) as bit) - ,[isSent] - ,[isFinal] - ,[hasFile] = CAST( CASE wHEN [file] is not null and [DocumentNAme] is not null THEN 1 ELSE 0 END as bit) - ,[fds] = @true - ,[customer_id] = iv.[CustomerId] - from [dbo].[fds__invoices] as iv - LEFT JOIN itm on iv.Id = itm.[invoice.Id] - LEFT JOIN sc on iv.[id] = sc.[invoice.Id] and sc.[fds] = 1 - LEFT JOIN contra_accounts as ca ON ISNULL(iv.[InvoiceVAT_1], 19.0) = ca.[vat] - where iv.Id in (SELECT [Id] FROM @inv Where fds = 1) - ), reminder as ( - SELECT [InvId], [lastReminder] = MAX([DateFinalized]), [#] = COUNT(*) FROM [dbo].[fds__reminder] WHERE [isFinal] = 1 AND [invId] IN (SELECT [Id] FROM @inv) GROUP BY [InvId] - ) - select - inv.* - ,[reminderstatus] = CASE WHEN [ispayed] = 1 THEN 0 - WHEN [iscanceled] = 1 THEN 0 - WHEN DueDate is null THEN 0 - WHEN [deb_cred] = 'H' THEN 0 - WHEN @stage3 <> '' AND rem.[#] = 2 AND @today > [dbo].[date_addcustom]([lastReminder], @stage3) THEN 3 - WHEN @stage2 <> '' AND rem.[#] = 1 AND @today > [dbo].[date_addcustom]([lastReminder], @stage2) THEN 2 - WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), ISNULL([PaymentTerm],@stage1)) THEN 1 - ELSE 0 END - ,[invstatus] = CASE WHEN [IsPayed] = 1 AND [isCanceled] = 0 THEN 'pyd' - WHEN [isCanceled] = 1 THEN 'cc' - WHEN [isFinal] = 0 THEN 'dft' - WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns' - WHEN [DueDate] is null THEN 'op' - WHEN [deb_cred] = 'H' THEN '' - WHEN @today >= [DueDate] THEN 'due' - WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, [DateOfCreation]), @stage1) THEN 'ovd' - WHEN ISNULL(rem.[#],0) > 0 THEN 'rem' - END - ,[reminder] = rem.[#] - ,[CustomerName] = c.[name] - ,[order] = CASE WHEN @include_drafts = 1 THEN - ROW_NUMBER() OVER (ORDER BY inv.[DateOfCreation] ASC) - ELSE - ROW_NUMBER() OVER (ORDER BY TRY_CAST(SUBSTRING([invoiceid], 2,4) AS int), TRY_CAST(SUBSTRING([invoiceid], 7,4) AS int), inv.[DateOfCreation] ASC) - END - ,[Payment] = IIF(py.[amount] is null,'',FORMAT(ISNULL(py.[amount],0.0), '0.00€', 'de') + IIF(py.[manu] = 1, ' man.','')) - /*CASE WHEN [paymentstatus] = 'a' THEN 'vollst.' - WHEN [paymentstatus] = 'p' THEN FORMAT(ISNULL([dbo].[fds__fn_InvoicePaymentAmount](inv.[Id]),0.0), '0.00€', 'de') - ELSE '' END*/ - FROM inv - left JOIN reminder as rem ON inv.[Id] = rem.[InvId] --inv.[id] is already converted to varchar - LEFT JOIN [dbo].[mfr__companies] as c ON inv.[customer_id] = c.[id] - LEFT JOIN [dbo].[fds__fn_bankingtransactions_perInvoice](@startdate, @enddate) as py ON py.[invid] = inv.[Id] - WHERE ((@only_reminded = 0 AND @only_remindedunpayed = 0) OR ISNULL(rem.[#],0) > 0) - AND (CASE WHEN @only_unpayed = 0 AND @only_remindedunpayed = 0 THEN 1 WHEN [IsCanceled] = 1 THEN 0 ELSE 1 END) = 1 - order by inv.[DateOfCreation] ASC; - - - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getReminder.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getReminder.sql deleted file mode 100644 index dfaeac7..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getReminder.sql +++ /dev/null @@ -1,96 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getReminder] - @Id varchar(8), - @includefile bit, - @authuser varchar(25) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - - DECLARE @rem [dbo].[fds__tt__reminder_core]; - - - INSERT INTO @rem - SELECT TOP(1) [Id] - ,[Version] - ,[DocumentName] - ,[InvId] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[type] - ,[amount] - ,[amount_payed] - ,[amount_open] - ,[subject] - ,[text] - ,[IsSent] - ,[IsFinal] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified] - ,[file] = CASE WHEN ISNULL(@includefile,0) = 1 THEN [file] else NULL END -- do not return file if not explicitly requested - FROM [dbo].[fds__reminder] - WHERE [Id] = @Id AND @Id is not null; - - - - DECLARE @InvId varchar(15) = (SELECT TOP(1) [InvId] fROM @rem); - - ---output - - --admin - SELECT TOP(1) - [today] - , [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf' - , [CustomerId] = rem.[CustomerId] - , [type] = rem.[type] - FROM (VALUES(CAST(GETDATE() as date)))base ([today]) cross JOIN @rem as rem; - - --rem - With inv1 as( - select TOP(1) * FROM [dbo].[fds__invoices] WHERE [id] = @InvId - ), inv as( - SELECT [Id] = cast([id] as varchar(15)) - ,[Invoiceid] - ,[DateFinalized] - ,[InvoiceBalance] - ,[DocumentName] - ,[file] - FROM inv1 - UNION - SELECT [Id] = cast([id] as varchar(15)) - ,[Invoiceid] - ,[DateFinalized] = [DateOfCreation] - ,[InvoiceBalance] - ,[DocumentName] - ,[file] = NULL - FROM [dbo].[mfr__invoices] as mfri - WHERE [id] = TRY_CAST(@invId as bigint) AND NOT EXISTS( SELECT 0 FROM inv1 ) --will only be used, if id not present in fds__invoices - ) - SELECT TOP(1) o.* - ,inv.[InvoiceId] - ,[InvoiceDate] = inv.[DateFinalized] - ,[InvoiceFileName] = inv.[DocumentName] - ,[InvoiceFile] = CASE WHEN ISNULL(@includefile,0) = 1 THEN inv.[file] else NULL END -- do not return file if not explicitly requested - ,[hasFile] = CAST ( CASE WHEN o.[file] is null THEN 0 ELSE 1 END as bit) - ,[UserNameFinalized] = [dbo].[fis_admin_getUserName_byID](o.[UserFinalized]) - ,[UserEmailFinalized] = [dbo].[fis_admin_getUserEmail_byID](o.[UserFinalized]) - from @rem as o - join inv on o.[invid] = inv.[id]; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getReportDocument.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getReportDocument.sql deleted file mode 100644 index 60d433e..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getReportDocument.sql +++ /dev/null @@ -1,26 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getReportDocument] - - @reportid varchar(20) -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @idbigint bigint = TRY_CAST(@reportid as bigint); - - SELECT TOP(1) - [Id] = CAST([Id] as varchar(20)),[InvoiceID],[URI],[DocumentName],[isInvoice] = 1,[DateOfCreation],[fds] = CAST( 0 as bit),[file] = CAST( null as varbinary(max)) - ,[order] = ROW_NUMBER() OVER (ORDER BY [DateOfCreation] ASC) - FROM [dbo].[mfr__invoices] WHERE [id] = @idbigint and @idbigint is not null - UNION - SELECT TOP(1) - [Id] = CAST([Id] as varchar(20)),[InvoiceID],[URI] = '',[DocumentName],[isInvoice] = 1,[DateOfCreation] = [DateCreated],[fds] = CAST( 1 as bit), [file] - ,[order] = ROW_NUMBER() OVER (ORDER BY [DateCreated] ASC) - FROM [dbo].[fds__invoices] WHERE [id] = @reportid AND [isFinal] = 1; - ; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequest_details.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequest_details.sql deleted file mode 100644 index 4b97694..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequest_details.sql +++ /dev/null @@ -1,422 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getRequest_details] - @servicerequestid bigint - , @mode varchar(3) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - IF [dbo].[fis_getModuleAuth]('fds_req', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - - - DECLARE @today date = GETDATE(); - DECLARE @SReq TABLE ( - [Id] [bigint] NOT NULL, - [Name] [nvarchar](255) NULL, - [ExternalId] [nvarchar](255) NULL, - [InvoiceId] [nvarchar](255) NULL, - [ClosedAt] [datetime] NULL, - [ReleasedAt] [datetime] NULL, - [WorkDoneAt] [datetime] NULL, - [TargetTimeInMinutes] [bigint] NOT NULL, - [DateModified] [datetime] NULL, - [DateOfCreation] [datetime] NULL, - [DueDateRangeStart] [datetime] NULL, - [DueDateRangeEnd] [datetime] NULL, - [PortalLink] [nvarchar](255) NULL, - [CostCenterId] [bigint] NULL, - [Description] [nvarchar](max) NULL, - [State] [nvarchar](255) NULL, - [CustomValues] [nvarchar](max) NOT NULL, - [CurrentOwnerId] [bigint] NULL, - [CustomerId] [bigint] NULL, - [ParentServiceRequestId] [bigint] NULL, - [ParentExtenalId] [nvarchar](255) NULL, - [ParentWorkDoneAt] [date] NULL, - [ParentInvoiceId] [nvarchar](255) NULL, - [Location#ID] [bigint] NULL, - [Version] [int] NOT NULL, - [IsTemplate] [bit] NOT NULL, - [IsTemplateMobile] [bit] NOT NULL, - [CreateFromServiceRequestTemplateId] [bigint] NOT NULL, - [Type] [nvarchar](255) NULL, - [Summary] nvarchar(255) NULL, - [isChild] bit NULL, - [baseorder] int, - [provision] varchar(255) NULL, - [open] bit NULL - ); - insert into @SReq - SELECT - [Id] - ,[Name] - ,[ExternalId] - ,[InvoiceId] - ,[ClosedAt] - ,[ReleasedAt] - ,[WorkDoneAt] - ,[TargetTimeInMinutes] - ,[DateModified] - ,[DateOfCreation] - ,[DueDateRangeStart] - ,[DueDateRangeEnd] - ,[PortalLink] - ,[CostCenterId] - ,[Description] - ,[State] - ,[CustomValues] - ,[CurrentOwnerId] - ,[CustomerId] - ,[ParentServiceRequestId] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then [id] ELSE [ParentServiceRequestId] END - ,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255)) - ,[ParentWorkDoneAt] = cast(NULL as datetime) - ,[ParentInvoiceId] = cast(NULL as nvarchar(255)) - ,[Location#ID] - ,[Version] - ,[IsTemplate] - ,[IsTemplateMobile] - ,[CreateFromServiceRequestTemplateId] - ,[Type] - ,[Summary] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[baseorder] = 0 - ,[provision] = NULL - ,[open] = 1 - FROM [dbo].[mfr__servicerequests] as s - WHERE [id] = @servicerequestid; - - INSERT INTO @SReq - SELECT s.[Id] - ,s.[Name] - ,s.[ExternalId] - ,s.[InvoiceId] - ,s.[ClosedAt] - ,s.[ReleasedAt] - ,s.[WorkDoneAt] - ,s.[TargetTimeInMinutes] - ,s.[DateModified] - ,s.[DateOfCreation] - ,s.[DueDateRangeStart] - ,s.[DueDateRangeEnd] - ,s.[PortalLink] - ,s.[CostCenterId] - ,s.[Description] - ,s.[State] - ,s.[CustomValues] - ,s.[CurrentOwnerId] - ,s.[CustomerId] - ,s.[ParentServiceRequestId] - ,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END - ,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END - ,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END - ,s.[Location#ID] - ,s.[Version] - ,s.[IsTemplate] - ,s.[IsTemplateMobile] - ,s.[CreateFromServiceRequestTemplateId] - ,s.[Type] - ,[Summary] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[baseorder] = 0 - , [provision] = null - ,[open] = 1 - from [dbo].[mfr__servicerequests] as s JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs - WHERE NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id]) - ; - - -- need to use CTE, because row_number is not available with update statements - WITH q as ( - SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), ISNULL([ParentServiceRequestId],[id])) * 100) - FROM @SReq as s - WHERE [ischild] = 0 - ) - UPDATE s SET - [baseorder] = q.baseorder - FROM @SReq as s JOIN q ON s.[id] = q.[id]; - - - -- need to use CTE, because row_number is not available with update statements - WITH q as ( - SELECT s.[id] - , q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId] - , [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation]) - FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id] - WHERE s.[ischild] = 1 - ) - UPDATE s SET - [ParentExtenalId] = q.[ExternalId] - ,[ParentWorkDoneAt] = q.[WorkDoneAt] - ,[ParentInvoiceId] = q.[InvoiceId] - ,[baseorder] = q.[baseorder] - FROM @SReq as s JOIN q ON s.[Id] = q.[id]; - - - DECLARE @inv TABLE ([Id] bigint, [Invoices] varchar(1000), [isOpen] bit); - With fdsinv as( - SELECT [Id] = fdsr.[mfr__servicerequest], fdsi.[InvoiceId], fdsi.[InvoiceType], [isCanceled] = ISNULL(fdsi.[isCanceled],0) - FROM [dbo].[fds__invoice_servicerequests] as fdsr - JOIN [dbo].[fds__invoices] as fdsi on fdsr.[invid] = fdsi.[id] - JOIN @SReq as s ON fdsr.[mfr__servicerequest] = s.[id] - WHERE [isFinal] = 1 AND fdsr.[mfr__servicerequest] is not null - - ), fi as( - SELECT [Id], [isOpen] = MIN(CASE WHEN ISNULL([invoicetype],'r') = 'r' AND [isCanceled] = 0 THEN 0 ELSE 1 END) FROM fdsinv GROUP BY [Id] - ) - INSERT INTO @inv - SELECT [Id] - , [invoices] = ISNULL(STUFF( (SELECT CHAR(10) + [InvoiceId] + ' (' + [InvoiceTYpe] + ')' FROM fdsinv as f2 WHERE f2.[id] = fi.[Id] FOR XML PATH, TYPE).value('.[1]','nvarchar(max)') ,1,1,'') , '') - , [isOpen] - FROM fi; - - UPDATE s - SET s.[invoiceid] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices]) - ,[open] = 1 --CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END - FROM @SReq as s LEFT JOIN @inv as inv on s.[id] = inv.[id]; - - - - - DECLARE @step TABLE ( - [Id] [bigint] NOT NULL, - [MobileId] [bigint] NULL, - [Version] [int] NOT NULL, - [Name] [nvarchar](255) NULL, - [IsDone] [bit] NOT NULL, - [HasError] [bit] NOT NULL, - [TrackingId] [nvarchar](255) NULL, - [Type] [nvarchar](255) NULL, - [SortOrder] [int] NOT NULL, - [Data] [nvarchar](max) NULL, - [DateModifiedOffline] [datetime] NULL, - [ServiceRequestId] [bigint] NULL, - [Description] [nvarchar](max) NULL, - [Comment] [nvarchar](max) NULL, - [InternalComment] [nvarchar](max) NULL, - [ServiceObjectId] [bigint] NULL, - [StepListTemplateId] [bigint] NULL, - [ParentId] [bigint] NULL, - [order] int NOT NULL - ); - DECLARE @company TABLE ( - [Id] [bigint] NOT NULL, - [Location#ID] [bigint] NULL, - [Version] [int] NOT NULL, - [IsPhysicalPerson] [bit] NOT NULL, - [IsOwner] [bit] NOT NULL, - [IsEmailInvoicingActive] [bit] NOT NULL, - [IsSupplier] [bit] NOT NULL, - [MappingId] [nvarchar](255) NULL, - [ExternalId] [nvarchar](255) NULL, - [Name] [nvarchar](255) NULL, - [Note] [nvarchar](255) NULL, - [SupportTelephone] [nvarchar](255) NULL, - [SupportFax] [nvarchar](255) NULL, - [SupportMail] [nvarchar](255) NULL, - [QuickSearch] [nvarchar](255) NULL, - [DateModified] [datetime] NOT NULL, - [CustomValues] [nvarchar](max) NOT NULL, - [MainContactId] [bigint] NOT NULL, - [address] nvarchar(1000) null - ); - INSERT INTO @company SELECT DISTINCT cy.*, [address] = [dbo].[fds__getCompanyAddress](cy.[id]) FROM [mfr__companies] as cy JOIN @SReq as s on cy.id = s.[CustomerId]; - DECLARE @companyids [dbo].[fds__tt__idlist]; - INSERT INTO @companyids SELECT DISTINCT [id] FROM @company; - - /** OUTPUT + get other types **/ - - IF @mode in ('r','ov', '','f', 'ful') - BEGIN - - WITH app as( - SELECT [start] = MIN(CAST(CASE WHEN a.[StartDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[StartDateTime] as date) > s.[workdoneat] THEN CAST(a.[StartDateTime] as date) ELSE s.[workdoneat] END as date)) - , [end] = MAX(CAST(CASE WHEN a.[EndDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[EndDateTime] as date) > s.[workdoneat] THEN CAST(a.[EndDateTime] as date) ELSE s.[workdoneat] END as date)) - , [CustomerId] = MAX (s.customerid) - , [ParentServiceRequestId] = ISNULL(MIN(CASE WHEN [isChild] = 1 then null else s.[Id] END), MAX([ParentServiceRequestId])) --make sure only one returned - FROM [dbo].[mfr__appointments] as a - RIGHT JOIN @SReq as s on a.[ServiceRequestId] = s.[Id] AND a.[State] in ('IsWorkDone') - ) - SELECT TOP(1) - [today] - , [provisionstart] = CASe WHEN [start] = [end] THEN NULL ELSE [start] END - , [provisionend] = [end] - , [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf' - , [paymentterms] = N'10wd' - , [invoiceemail] = (SELECT TOP(1) [SupportMail] FROM @company where IsEmailInvoicingActive = 1) - , [invoiceaddress] = (SELECT TOP(1) CONCAT([name], CHAR(10), [address]) FROM @company ORDER BY IsEmailInvoicingActive DESC) - , [tax_servicerefund] = 0.2 - , [CustomerId] = [CustomerId] - , [search] = @servicerequestid - , [parent] = app.[ParentServiceRequestId] - FROM (VALUES(CAST(GETDATE() as date)))base ([today]) CROSS JOIN app; - - SELECT - * - ,[order] = ROW_NUMBER() OVER (ORDER BY [baseorder]) - FROM @SReq; - END - - IF @mode in ('i','ov') - BEGIN - SELECT - itm.[Id] - , [net_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2)) , '#0.00 €','de') END - , [bo_pos] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN '' ELSE FORMAT( CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) , '#0.00 €','de') END - , [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END - , itm.[ServiceRequestId] - , itm.[SortOrder] - , itm.[Type] - , itm.[NameOrNumber] - , itm.[Note] - , [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder]) - FROM [dbo].[mfr__items] as itm - JOIN @SReq as s ON itm.ServiceRequestId = s.[Id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item' - ; - END - ELSE IF @mode in ('f', 'ful') - BEGIN - SELECT - itm.[Id] - , [net] = ISNULL([Price],0) - , [quantityhours] = ISNULL([quantityhours],1) - , itm.[UnitString] - , [net_val] = CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2)) - , [bo_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) - , [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END - , [vat_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * ((ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) - , itm.[ServiceRequestId] - , itm.[SortOrder] - , itm.[Type] - , itm.[NameOrNumber] - , itm.[Note] - , [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id]) - FROM [dbo].[mfr__items] as itm - JOIN @SReq as s ON itm.ServiceRequestId = s.[Id] - LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item' - ; - END - - IF @mode in ('f', 'ful') - BEGIN - INSERT INTO @step - SELECT - stp.* - , [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY stp.[ServiceRequestId] ORDER BY stp.[SortOrder], stp.[Id]) - FROM [dbo].[mfr__steps] as stp JOIN @SReq as s ON stp.ServiceRequestId = s.[Id]; - SELECT * from @step; - - - - SELECT - cc.[Id] - , [CompanyName] = cc.[name] - , ct.[Gender] - , ct.[FirstName] - , ct.[LastName] - , ct.[Email] - , cc.[address] - FROM [dbo].[fds__getCompanysContacts](@companyids) as ct join @company as cc on cc.[Id] = ct.[CyId]; - - - - SELECT DISTINCT - [ref] = 'Einsatzort ' + so.[name] - ,[address] = [dbo].[fds__fn_combineAddress] (so.[Name], loc.[AddressString], null - ,loc.[Postal] - ,loc.[City] - ,loc.[State] - ,loc.[Country]) - FROM @SReq as s - LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:ServiceObjects' and p.[EntityId] = s.[id] - LEFT JOIN [mfr__serviceobjects] as so ON so.id = p.[PartnerId] - LEFT JOIN [mfr__*PartnerSet] as po ON p.property = 'ServiceObject:Location' and po.[EntityId] = so.[Id] - LEFT join [dbo].[mfr__#locations] as loc ON (loc.[id] = so.[Location#ID] OR po.[PartnerId] = loc.[Id] OR (so.[Id] = loc.[EntityId] and loc.Property = 'ServiceObject:Location')) - WHERE so.id is not null and loc.[id] is not null - UNION - SELECT DISTINCT - [ref] = 'Auftragsort ' + [name] - ,[address] = [dbo].[fds__fn_combineAddress] (loc.[AddressString], null, null - ,loc.[Postal] - ,loc.[City] - ,loc.[State] - ,loc.[Country]) - FROM @SReq as s - LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:Location' and p.[EntityId] = s.[id] - join [dbo].[mfr__#locations] as loc ON (s.[Location#ID] = loc.[id] OR p.[PartnerId] = loc.[Id] OR (s.[Id] = loc.[EntityId] and loc.Property = 'ServiceRequest:Location')) - WHERE loc.[id] is not null - UNION - SELECT DISTINCT [ref] = 'Kunde' - , [address] - FROM @company; - - - - END - - - --get invoices - - --Reminder settings - DECLARE @stage1 varchar(10), @stage2 varchar(10), @stage3 varchar(10); - SELECT @stage1 = [stage1], @stage2 = [stage2], @stage3 = [stage3] FROM [dbo].[fds__admin_reminderSettings](); - With inv as( - SELECT [id] = CAST(ir.invid as varchar(20)), [isChild] = CAST(MIN(CAST(s.[isChild] as tinyint)) as BIT), [baseorder] = MIN(s.[baseorder]), [sopen] = CAST( MIN( CAST(s.[open] as tinyint) ) as bit) - FROM [dbo].[fds__invoice_servicerequests] as ir - JOIN @sreq as s ON ir.[mfr__servicerequest] = s.[id] - GROUP by ir.invid - UNION - SELECT [id] = CAST(i.[id] as varchar(20)), [isChild] = CAST(MIN(CAST(s.[isChild] as tinyint)) as BIT), [baseorder] = MIN(s.[baseorder]), [sopen] = CAST( MIN( CAST(s.[open] as tinyint) ) as bit) - FROM [dbo].[mfr__servicerequests] as ms JOIN @sreq as s on ms.[id] = s.[id] - JOIN [mfr__invoices] as i on ms.[InvoiceId] like ('%' + i.[InvoiceId] + '%') - GROUP by i.[id] - ),reminder as ( - SELECT [InvId], [lastReminder] = MAX([DateSent]), [#] = COUNT(*) FROM [dbo].[fds__reminder] WHERE [isSent] = 1 AND [invId] IN (SELECT [Id] FROM inv) GROUP BY [InvId] - ) - SELECT [Id] = CAST(i.[Id] as varchar(20)) - ,i.[DateCreated] - ,[isFinal] - ,[isSent] - ,i.[DocumentName] - ,[invstatus] = CASE WHEN [IsPayed] = 1 AND [isCanceled] = 0 THEN 'pyd' - WHEN [isCanceled] = 1 OR [dbo].[fds__fn_isInvoiceCancelled](inv.[Id]) = 1 THEN 'cc' - WHEN [isFinal] = 0 THEN 'dft' - WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns' - WHEN [DueDate] is null THEN 'op' - WHEN [invoiceBalance] < 0 THEN '' - WHEN @today >= [DueDate] THEN 'due' - WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, i.[DateCreated]), @stage1) THEN 'ovd' - WHEN ISNULL(rem.[#],0) > 0 THEN 'rem' - END - ,[order] = ROW_NUMBER() OVER ( ORDER BY inv.[isChild], inv.[baseorder], i.[isFinal] DESC, i.[datecreated] ASC ) - FROM [dbo].[fds__invoices] as i - JOIN inv on i.[Id] = inv.[Id] - LEFT JOIN reminder as rem on inv.id = rem.[InvId] - WHERE (inv.[sopen] = 1 OR i.[isfinal] = 1) --- don't show any draft invoices for final tasks; - UNION - SELECT [Id] = CAST(i.[Id] as varchar(20)) - ,[DateCreated] = i.[DateOfCreation] - ,[isFinal] = 1 - ,[isSent] = 1 - ,i.[DocumentName] - ,[invstatus] = CASE WHEN [invoicestate] in ('eIsPaid') THEN 'pyd' - WHEN [invoicestate] in ('eIsCancelled') THEN 'cc' - --WHEN [isFinal] = 0 THEN 'dft' - --WHEN [isFinal] = 1 AND [isSent] = 0 THEN 'uns' - WHEN [DueDate] is null THEN 'op' - WHEN @today >= [DueDate] THEN 'due' - WHEN ISNULL(rem.[#],0) > 0 THEN 'rem' - WHEN @stage1 <> '' AND ISNULL(rem.[#],0) = 0 AND @today > [dbo].[date_addcustom](ISNULL(DueDate, i.[DateofCreation]), @stage1) THEN 'ovd' - END - ,[order] = ROW_NUMBER() OVER ( ORDER BY inv.[isChild], inv.baseorder, i.[dateofcreation] ASC) - FROM [dbo].[mfr__invoices] as i - JOIN inv on TRY_CAST(i.[Id] as varchar(20)) = inv.[Id] - LEFT JOIN reminder as rem on inv.id = rem.[InvId]; -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequests_list.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequests_list.sql deleted file mode 100644 index 65d9b2c..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequests_list.sql +++ /dev/null @@ -1,278 +0,0 @@ - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getRequests_list] - @tgtdate date - ,@mode varchar(1) = '' - ,@completed bit = 1 - ,@authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_req', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__getRequests_list' ,@authuser , ''); - - - - DECLARE @startdate date, @enddate date, @today date = GETDATE(); - SET @mode = ISNULL(LOWER(@mode),''); - If @mode in ('r') -- r = open recent - BEGIN - SET @tgtdate = @today; - SELECT @startdate = DATEADD(week, -4, [dbo].[date_weekfirst](@tgtdate)) - ,@enddate = @today; - END - else If @mode in ('m') -- r = open recent - BEGIN - SELECT @startdate = [dbo].[date_monthfirst](@tgtdate) - ,@enddate = [dbo].[date_monthend](@tgtdate); - END - ELSE If @mode = 'w' - BEGIN - SELECT @startdate = [dbo].[date_weekfirst](@tgtdate) - ,@enddate = [dbo].[date_weekend](@tgtdate); - END - - DECLARE @openmode as bit = CASE WHEN LOWER(LEFT(ISNULL(@mode, ''),1)) IN ('o', 'r') THEN 1 ELSE 0 END; - SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode - , [title] = CASE WHEN @openmode = 1 AND @mode = 'r' THEN 'Übersicht der offenen Aufträge (letzte 4 Wochen seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ')' - WHEN @openmode = 1 THEN 'Übersicht der offenen Aufträge' - ELSE 'Auftragsübersicht für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy') - WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.') - ELSE '' END - END - , [note] = CASE WHEN @openmode = 1 and @mode = 'r' THEN 'Es werden alle Aufträge angezeigt, die selbst offen sind und seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden, oder deren Folgeauftrag offen ist und/oder seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden.' - WHEN @openmode = 1 THEN 'Es werden alle Aufträge angezeigt, die selbst oder ein Folgeeauftrag offen ist.' - ELSE '' END; - - - DECLARE @SReq TABLE ( - [Id] [bigint] NOT NULL, - [Name] [nvarchar](255) NULL, - [ExternalId] [nvarchar](255) NULL, - [InvoiceId] varchar(1000) NULL, - [ClosedAt] [datetime] NULL, - [ReleasedAt] [datetime] NULL, - [WorkDoneAt] [datetime] NULL, - [TargetTimeInMinutes] [bigint] NULL, - [DateModified] [datetime] NULL, - [DateOfCreation] [datetime] NULL, - [DueDateRangeStart] [datetime] NULL, - [DueDateRangeEnd] [datetime] NULL, - [PortalLink] [nvarchar](255) NULL, - [CostCenterId] [bigint] NULL, - [Description] [nvarchar](max) NULL, - [State] [nvarchar](255) NULL, - [CustomValues] [nvarchar](max) NULL, - [CurrentOwnerId] [bigint] NULL, - [CustomerId] [bigint] NULL, - [ParentServiceRequestId] [bigint] NULL, - [ParentExtenalId] [nvarchar](255) NULL, - [ParentWorkDoneAt] [date] NULL, - [ParentInvoiceId] varchar(1000) NULL, - [Location#ID] [bigint] NULL, - [Version] [int] NULL, - [IsTemplate] [bit] NULL, - [IsTemplateMobile] [bit] NULL, - [CreateFromServiceRequestTemplateId] [bigint] NULL, - [Type] [nvarchar](255) NULL, - [CustomerName] [nvarchar](255) NULL, - [isChild] bit NULL, - [open] bit, - [baseorder] int - ); - DECLARE @inv TABLE ([Id] bigint, [Invoices] varchar(1000), [isOpen] bit); - With fdsinv as( - SELECT [Id] = fdsr.[mfr__servicerequest], fdsi.[InvoiceId], fdsi.[InvoiceType], [isCanceled] = ISNULL(fdsi.[isCanceled],0) FROM [dbo].[fds__invoice_servicerequests] as fdsr - JOIN [dbo].[fds__invoices] as fdsi on fdsr.[invid] = fdsi.[id] - WHERE [isFinal] = 1 AND fdsr.[mfr__servicerequest] is not null - ), fi as( - SELECT [Id], [isOpen] = MIN(CASE WHEN ISNULL([invoicetype],'r') = 'r' AND [isCanceled] = 0 THEN 0 ELSE 1 END) FROM fdsinv GROUP BY [Id] - ) - INSERT INTO @inv - SELECT [Id] - , [invoices] = ISNULL(STUFF( (SELECT CHAR(10) + [InvoiceId] + ' (' + [InvoiceTYpe] + ')' FROM fdsinv as f2 WHERE f2.[id] = fi.[Id] FOR XML PATH, TYPE).value('.[1]','nvarchar(max)') ,1,1,'') , '') - , [isOpen] - FROM fi; - - - insert into @SReq - SELECT - s.[Id] - ,[Name] - ,[ExternalId] - ,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices]) - ,[ClosedAt] - ,[ReleasedAt] - ,[WorkDoneAt] - ,[TargetTimeInMinutes] - ,s.[DateModified] - ,s.[DateOfCreation] - ,[DueDateRangeStart] - ,[DueDateRangeEnd] - ,[PortalLink] - ,[CostCenterId] - ,[Description] - ,[State] - ,s.[CustomValues] - ,[CurrentOwnerId] - ,s.[CustomerId] - ,[ParentServiceRequestId] - ,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255)) - ,[ParentWorkDoneAt] = cast(NULL as datetime) - ,[ParentInvoiceId] = cast(NULL as nvarchar(255)) - ,[Location#ID] - ,s.[Version] - ,[IsTemplate] - ,[IsTemplateMobile] - ,[CreateFromServiceRequestTemplateId] - ,[Type] - ,[CustomerName] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END - ,[baseorder] = 0 - FROM [dbo].[mfr__servicerequests] as s -- parent and childs; - LEFT JOIN @inv as inv on inv.[Id] = s.[Id] - WHERE - ([type] in ('IsProject', 'IsMaintenance') - AND - (CASE WHEN @openmode = 1 AND @mode = 'r' THEN - ( CASE WHEN - [workdoneat] is not null - --AND ISNULL(s.[invoiceid],'') = '' - AND ISNULL([State], '') <> 'Closed' - --AND ISNULL(inv.[isOpen],1) = 1 - AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1) - AND (CAST([DateOfCreation] as date) BETWEEN @startdate AND @today OR CAST([DateModified] as date) BETWEEN @startdate AND @today) - AND EXISTS (SELECT 0 FROM [dbo].[mfr__*PartnerSet] as ps JOIN [dbo].[mfr__tags] as tg on ps.Property = 'ServiceRequest:Tags' and tg.Id = ps.[PartnerId] where EntityId = s.[id] and ps.partnerid = 16034758659 and [Type] = 'ServiceRequest') - THEN 1 ELSE 0 END) - WHEN @openmode = 1 AND @mode <> 'r' THEN - ( CASE WHEN - [workdoneat] is not null - AND ISNULL(s.[invoiceid],'') = '' - AND ISNULL([State], '') <> 'Closed' - AND ISNULL(inv.[isOpen],1) = 1 - AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1) - THEN 1 ELSE 0 END) - WHEN @openmode = 0 THEN - ( CASE WHEN CAST([DateOfCreation] as date) BETWEEN @startdate AND @enddate THEN 1 ELSE 0 END) - ELSE 0 END - ) = 1); - - INSERT INTO @SReq - SELECT DISTINCT - ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END) - ,ISNULL(s.[Name], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN ' - - unbekannt - - ' ELSE NULL END) - ,s.[ExternalId] - ,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices]) - ,s.[ClosedAt] - ,s.[ReleasedAt] - ,s.[WorkDoneAt] - ,s.[TargetTimeInMinutes] - ,s.[DateModified] - ,s.[DateOfCreation] - ,s.[DueDateRangeStart] - ,s.[DueDateRangeEnd] - ,s.[PortalLink] - ,s.[CostCenterId] - ,s.[Description] - ,s.[State] - ,s.[CustomValues] - ,s.[CurrentOwnerId] - ,s.[CustomerId] - ,s.[ParentServiceRequestId] - ,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END - ,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END - ,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END - ,s.[Location#ID] - ,s.[Version] - ,s.[IsTemplate] - ,s.[IsTemplateMobile] - ,s.[CreateFromServiceRequestTemplateId] - ,s.[Type] - ,[CustomerName] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END - ,[baseorder] = 0 - from [dbo].[mfr__servicerequests] as s RIGHT JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs - LEFT JOIN @inv as inv on inv.[Id] = s.[Id] - WHERE - (s.[type] in ('IsProject', 'IsMaintenance') - AND NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id]) - AND ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END) is not null - ) - ; - - - WITH q as ( - SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), CASE WHEN ISNULL([ParentServiceRequestId],0) = 0 THEN [id] ELSE [ParentServiceRequestId] END) * 100) - FROM @SReq as s - WHERE [ischild] = 0 - ) - UPDATE s SET - [baseorder] = q.baseorder - ,[ParentServiceRequestId] = CASE WHEN s.isChild = 0 THEN s.[Id] ELSE s.[ParentServiceRequestId] END - FROM @SReq as s JOIN q ON s.[id] = q.[id]; - - - UPDATE s SET - [CustomerName] = c.[name] - FROM @SReq as s JOIN [mfr__companies] as c ON s.[CustomerId] = c.[id]; - - - WITH q as ( - SELECT s.[id] - , q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId] - , [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation], s.[Id]) - FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id] - WHERE s.[ischild] = 1 - ) - UPDATE s SET - [ParentExtenalId] = q.[ExternalId] - ,[ParentWorkDoneAt] = q.[WorkDoneAt] - ,[ParentInvoiceId] = q.[InvoiceId] - ,[baseorder] = q.[baseorder] - FROM @SReq as s JOIN q ON s.[Id] = q.[id]; - - - - ---OUTPUT - - WITH tgs as( - SELECT ps.[EntityId], tg.* FROM [dbo].[mfr__tags] as tg JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] = 'ServiceRequest:Tags' and tg.[Id] = ps.[PartnerId] AND ISNULL(tg.[Name],'') <> '' and [Type] = 'ServiceRequest' - WHERE tg.Id in (16288120843 - ,15842181123 - ,16034758659 - ,16050520064 - ,16783966210 - ,16802480128 - ,16830398466 - ,19650019330 - ,19670532096 - ,18925977600) - AND EXISTS (SELECT 0 FROM @SReq as _s where _s.Id = ps.[EntityId]) - ) - SELECT - s.* - ,[tags] = STUFF( (SELECT ',' + TRIM([Name]) FROM tgs where tgs.[EntityId] = s.[Id] order by tgs.[name] FOR XML PATH, TYPE).value('.[1]','varchar(255)'), 1,1,'') - FROM @SReq as s order by baseorder - - - - SELECT - [requestID] = s.[Id] - ,r.* - ,[order] = ROW_NUMBER() OVER (PARTITION BY s.[Id] ORDER BY r.[dateofcreation] ASC, r.[Id]) - FROM [dbo].[mfr__invoices] as r - JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] like 'ServiceRequest:Invoices' and r.[id] = ps.PartnerId - JOIN @SReq as s ON s.[Id] = ps.[EntityId] - ORDER BY s.[Id], r.[dateofcreation] ASC; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequests_list2.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequests_list2.sql deleted file mode 100644 index d27a3fb..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__getRequests_list2.sql +++ /dev/null @@ -1,308 +0,0 @@ - - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__getRequests_list2] - @tgtdate date - ,@mode varchar(1) = '' - ,@completed bit = 1 - , @search varchar(100) - ,@authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_req', @authuser) < 1 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__getRequests_list' ,@authuser , @search); - - - - DECLARE @startdate date, @enddate date, @today date = GETDATE(); - DECLARE @searchmode as varchar(1) = CASE WHEN SUBSTRING(ISNULL(@search, '') + ' ', 2,1) <> ':' THEN '' ELSE LOWER(LEFT(@search,1 )) END; - - IF @searchmode <> '' - BEGIN - SELECT @mode = 's', @search = SUBSTRING(@search,3, 1000); - END - ELSE - SET @mode = LOWER(@mode); - - If @mode in ('r') -- r = open recent - BEGIN - SET @tgtdate = @today; - SELECT @startdate = DATEADD(week, -4, [dbo].[date_weekfirst](@tgtdate)) - ,@enddate = @today; - END - else If @mode in ('m') -- r = open recent - BEGIN - SELECT @startdate = [dbo].[date_monthfirst](@tgtdate) - ,@enddate = [dbo].[date_monthend](@tgtdate); - END - ELSE If @mode = 'w' - BEGIN - SELECT @startdate = [dbo].[date_weekfirst](@tgtdate) - ,@enddate = [dbo].[date_weekend](@tgtdate); - END - - DECLARE @openmode as bit = CASE WHEN LOWER(LEFT(ISNULL(@mode, ''),1)) IN ('o', 'r') THEN 1 ELSE 0 END; - SELECT [startdate] = @startdate, [enddate] = @enddate, [mode] = @mode - , [title] = CASE WHEN @searchmode = 'n' THEN 'Übersicht der Aufträge mit der Auftragsnummer ' + @search - WHEN @openmode = 1 AND @mode = 'r' THEN 'Übersicht der offenen Aufträge (letzte 4 Wochen seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ')' - WHEN @openmode = 1 THEN 'Übersicht der offenen Aufträge' - ELSE 'Auftragsübersicht für ' + CASE WHEN @mode = 'm' THEN 'MONAT ' + FORMAT(@startdate, 'MMM yyyy') - WHEN @mode = 'w' THEN 'WOCHE ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' - ' + FORMAT(@enddate, 'dd.MM.') - ELSE '' END - END - , [note] = CASE WHEN @searchmode = 'n' THEN 'Es wird nur der Auftrag mit der Nummer ' + @search + ' angezeigt' - WHEN @openmode = 1 and @mode = 'r' THEN 'Es werden alle Aufträge angezeigt, die selbst offen sind und seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden, oder deren Folgeauftrag offen ist und/oder seit ' + FORMAT(@startdate, 'dd.MM.yyyy') + ' erstellt wurden.' - WHEN @openmode = 1 THEN 'Es werden alle Aufträge angezeigt, die selbst oder ein Folgeeauftrag offen ist.' - ELSE '' END; - - - DECLARE @SReq TABLE ( - [Id] [bigint] NOT NULL, - [Name] [nvarchar](255) NULL, - [ExternalId] [nvarchar](255) NULL, - [InvoiceId] varchar(1000) NULL, - [ClosedAt] [datetime] NULL, - [ReleasedAt] [datetime] NULL, - [WorkDoneAt] [datetime] NULL, - [TargetTimeInMinutes] [bigint] NULL, - [DateModified] [datetime] NULL, - [DateOfCreation] [datetime] NULL, - [DueDateRangeStart] [datetime] NULL, - [DueDateRangeEnd] [datetime] NULL, - [PortalLink] [nvarchar](255) NULL, - [CostCenterId] [bigint] NULL, - [Description] [nvarchar](max) NULL, - [State] [nvarchar](255) NULL, - [CustomValues] [nvarchar](max) NULL, - [CurrentOwnerId] [bigint] NULL, - [CustomerId] [bigint] NULL, - [ParentServiceRequestId] [bigint] NULL, - [ParentExtenalId] [nvarchar](255) NULL, - [ParentWorkDoneAt] [date] NULL, - [ParentInvoiceId] varchar(1000) NULL, - [Location#ID] [bigint] NULL, - [Version] [int] NULL, - [IsTemplate] [bit] NULL, - [IsTemplateMobile] [bit] NULL, - [CreateFromServiceRequestTemplateId] [bigint] NULL, - [Type] [nvarchar](255) NULL, - [CustomerName] [nvarchar](255) NULL, - [isChild] bit NULL, - [open] bit, - [baseorder] int - ); - DECLARE @inv TABLE ([Id] bigint, [Invoices] varchar(1000), [isOpen] bit); - With fdsinv as( - SELECT [Id] = fdsr.[mfr__servicerequest], fdsi.[InvoiceId], fdsi.[InvoiceType], [isCanceled] = ISNULL(fdsi.[isCanceled],0) FROM [dbo].[fds__invoice_servicerequests] as fdsr - JOIN [dbo].[fds__invoices] as fdsi on fdsr.[invid] = fdsi.[id] - WHERE [isFinal] = 1 AND fdsr.[mfr__servicerequest] is not null - ), fi as( - SELECT [Id], [isOpen] = MIN(CASE WHEN ISNULL([invoicetype],'r') = 'r' AND [isCanceled] = 0 THEN 0 ELSE 1 END) FROM fdsinv GROUP BY [Id] - ) - INSERT INTO @inv - SELECT [Id] - , [invoices] = ISNULL(STUFF( (SELECT CHAR(10) + [InvoiceId] + ' (' + [InvoiceTYpe] + ')' FROM fdsinv as f2 WHERE f2.[id] = fi.[Id] FOR XML PATH, TYPE).value('.[1]','nvarchar(max)') ,1,1,'') , '') - , [isOpen] - FROM fi; - - WITH s as ( - SELECT * FROM [dbo].[mfr__servicerequests] as _s - WHERE (CASE - WHEN @searchmode = 'n' THEN - ( CASE WHEN [ExternalId] = @search OR [ExternalId] like ('_[-]' + @search) THEN 1 ELSE 0 END ) - WHEN @openmode = 1 AND @mode = 'r' THEN - ( CASE WHEN - [workdoneat] is not null - --AND ISNULL(s.[invoiceid],'') = '' - AND ISNULL([State], '') <> 'Closed' - --AND ISNULL(inv.[isOpen],1) = 1 - AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = _s.[id] and sr.[hidden] = 1) - AND (CAST([DateOfCreation] as date) BETWEEN @startdate AND @today OR CAST([DateModified] as date) BETWEEN @startdate AND @today) - AND EXISTS (SELECT 0 FROM [dbo].[mfr__*PartnerSet] as ps JOIN [dbo].[mfr__tags] as tg on ps.Property = 'ServiceRequest:Tags' and tg.Id = ps.[PartnerId] where EntityId = _s.[id] and ps.partnerid = 16034758659 and [Type] = 'ServiceRequest') - THEN 1 ELSE 0 END) - WHEN @openmode = 1 AND @mode <> 'r' THEN -- will be covered later because invoice info is included - 1 - --( CASE WHEN - -- [workdoneat] is not null - -- AND ISNULL(s.[invoiceid],'') = '' - -- AND ISNULL([State], '') <> 'Closed' - -- AND ISNULL(inv.[isOpen],1) = 1 - -- AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1) - -- THEN 1 ELSE 0 END) - WHEN @openmode = 0 THEN - ( CASE WHEN CAST([DateOfCreation] as date) BETWEEN @startdate AND @enddate THEN 1 ELSE 0 END) - ELSE 0 END - ) = 1 - ) - insert into @SReq - SELECT - s.[Id] - ,[Name] - ,[ExternalId] - ,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices]) - ,[ClosedAt] - ,[ReleasedAt] - ,[WorkDoneAt] - ,[TargetTimeInMinutes] - ,s.[DateModified] - ,s.[DateOfCreation] - ,[DueDateRangeStart] - ,[DueDateRangeEnd] - ,[PortalLink] - ,[CostCenterId] - ,[Description] - ,[State] - ,s.[CustomValues] - ,[CurrentOwnerId] - ,s.[CustomerId] - ,[ParentServiceRequestId] - ,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255)) - ,[ParentWorkDoneAt] = cast(NULL as datetime) - ,[ParentInvoiceId] = cast(NULL as nvarchar(255)) - ,[Location#ID] - ,s.[Version] - ,[IsTemplate] - ,[IsTemplateMobile] - ,[CreateFromServiceRequestTemplateId] - ,[Type] - ,[CustomerName] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END - ,[baseorder] = 0 - FROM s -- parent and childs; - LEFT JOIN @inv as inv on inv.[Id] = s.[Id] - WHERE - ([type] in ('IsProject', 'IsMaintenance') - AND - (CASE - WHEN @openmode = 1 AND @mode <> 'r' THEN - ( CASE WHEN - [workdoneat] is not null - AND ISNULL(s.[invoiceid],'') = '' - AND ISNULL([State], '') <> 'Closed' - AND ISNULL(inv.[isOpen],1) = 1 - AND NOT EXISTS (SELECT * FROM [dbo].[fds__custom_servicerequest] as sr where sr.[EntityID] = s.[id] and sr.[hidden] = 1) - THEN 1 ELSE 0 END) - ELSE 1 END -- heres the difference, because main filter is applied in CTE - ) = 1); - - - INSERT INTO @SReq - SELECT DISTINCT - ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END) - ,ISNULL(s.[Name], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN ' - - unbekannt - - ' ELSE NULL END) - ,s.[ExternalId] - ,[InvoiceId] = [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid], inv.[Invoices]) - ,s.[ClosedAt] - ,s.[ReleasedAt] - ,s.[WorkDoneAt] - ,s.[TargetTimeInMinutes] - ,s.[DateModified] - ,s.[DateOfCreation] - ,s.[DueDateRangeStart] - ,s.[DueDateRangeEnd] - ,s.[PortalLink] - ,s.[CostCenterId] - ,s.[Description] - ,s.[State] - ,s.[CustomValues] - ,s.[CurrentOwnerId] - ,s.[CustomerId] - ,s.[ParentServiceRequestId] - ,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END - ,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END - ,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END - ,s.[Location#ID] - ,s.[Version] - ,s.[IsTemplate] - ,s.[IsTemplateMobile] - ,s.[CreateFromServiceRequestTemplateId] - ,s.[Type] - ,[CustomerName] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[open] = CASE WHEN s.[workdoneat] is not null AND [dbo].[fds__fn_InvoiceIDs_mfr](s.[invoiceid],'') = '' AND ISNULL(inv.[isOpen],1) = 1 THEN 1 ELSE 0 END - ,[baseorder] = 0 - from [dbo].[mfr__servicerequests] as s RIGHT JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs - LEFT JOIN @inv as inv on inv.[Id] = s.[Id] - WHERE - (s.[type] in ('IsProject', 'IsMaintenance') - AND NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id]) - AND ISNULL(s.[Id], CASE WHEN ISNULL(q.[ParentServiceRequestId],0) > 0 THEN q.[ParentServiceRequestId] ELSE NULL END) is not null - ) - ; - - - WITH q as ( - SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), CASE WHEN ISNULL([ParentServiceRequestId],0) = 0 THEN [id] ELSE [ParentServiceRequestId] END) * 100) - FROM @SReq as s - WHERE [ischild] = 0 - ) - UPDATE s SET - [baseorder] = q.baseorder - ,[ParentServiceRequestId] = CASE WHEN s.isChild = 0 THEN s.[Id] ELSE s.[ParentServiceRequestId] END - FROM @SReq as s JOIN q ON s.[id] = q.[id]; - - - UPDATE s SET - [CustomerName] = c.[name] - FROM @SReq as s JOIN [mfr__companies] as c ON s.[CustomerId] = c.[id]; - - - WITH q as ( - SELECT s.[id] - , q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId] - , [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation], s.[Id]) - FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id] - WHERE s.[ischild] = 1 - ) - UPDATE s SET - [ParentExtenalId] = q.[ExternalId] - ,[ParentWorkDoneAt] = q.[WorkDoneAt] - ,[ParentInvoiceId] = q.[InvoiceId] - ,[baseorder] = q.[baseorder] - FROM @SReq as s JOIN q ON s.[Id] = q.[id]; - - - - ---OUTPUT - - WITH tgs as( - SELECT ps.[EntityId], tg.* FROM [dbo].[mfr__tags] as tg JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] = 'ServiceRequest:Tags' and tg.[Id] = ps.[PartnerId] AND ISNULL(tg.[Name],'') <> '' and [Type] = 'ServiceRequest' - WHERE tg.Id in (16288120843 - ,15842181123 - ,16034758659 - ,16050520064 - ,16783966210 - ,16802480128 - ,16830398466 - ,19650019330 - ,19670532096 - ,18925977600) - AND EXISTS (SELECT 0 FROM @SReq as _s where _s.Id = ps.[EntityId]) - ) - SELECT - s.* - ,[tags] = STUFF( (SELECT ',' + TRIM([Name]) FROM tgs where tgs.[EntityId] = s.[Id] order by tgs.[name] FOR XML PATH, TYPE).value('.[1]','varchar(255)'), 1,1,'') - FROM @SReq as s order by baseorder - - - - SELECT - [requestID] = s.[Id] - ,r.* - ,[order] = ROW_NUMBER() OVER (PARTITION BY s.[Id] ORDER BY r.[dateofcreation] ASC, r.[Id]) - FROM [dbo].[mfr__invoices] as r - JOIN [dbo].[mfr__*PartnerSet] as ps ON ps.[Property] like 'ServiceRequest:Invoices' and r.[id] = ps.PartnerId - JOIN @SReq as s ON s.[Id] = ps.[EntityId] - ORDER BY s.[Id], r.[dateofcreation] ASC; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__lookupReminders.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__lookupReminders.sql deleted file mode 100644 index 1230c57..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__lookupReminders.sql +++ /dev/null @@ -1,113 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__lookupReminders] - @InvId varchar(15) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 2 - THROW 60000, N'not authorized', 1; - ELSE IF NOT EXISTS (select 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId) AND NOT EXISTS (select 0 FROM [dbo].[mfr__invoices] WHERE [id] = TRY_CAST(@InvId as bigint)) - THROW 60000, N'invoice not found', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__lookupReminders' ,@authuser , (SELECT * FROM (VALUES(@InvId, @authuser)) as z ([InvId],[authuser]) FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER)); - - DECLARE @REM TABLE ( - [Id] [varchar](10) NOT NULL, - [Version] [int] NOT NULL, - [DocumentName] [varchar](100) NULL, - [InvId] [varchar](15) NOT NULL, - [CustomerId] [bigint] NULL, - [SendToAddress] [nvarchar](1000) NULL, - [SendToEmail] [nvarchar](255) NULL, - [type] [varchar](3) NOT NULL, - [amount] [numeric](10, 3) NULL, - [amount_payed] [numeric](10, 3) NULL, - [amount_open] [numeric](10, 3) NULL, - [subject] [nvarchar](255) NULL, - [text] [nvarchar](2000) NULL, - [IsSent] [bit] NOT NULL, - [IsFinal] [bit] NULL, - [CustomValues] [nvarchar](max) NULL, - [DateSent] [datetime] NULL, - [UserSent] [varchar](25) NULL, - [DateFinalized] [datetime] NULL, - [UserFinalized] [varchar](25) NULL, - [DateCreated] [datetime] NOT NULL, - [UserCreated] [varchar](25) NOT NULL, - [DateModified] [datetime] NOT NULL, - [UserModified] [varchar](25) NOT NULL, - [order] int - ); - - INSERT INTO @REM - ([Id] - ,[Version] - ,[DocumentName] - ,[InvId] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[type] - ,[amount] - ,[amount_payed] - ,[subject] - ,[text] - ,[IsSent] - ,[IsFinal] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified] - ,[order]) - SELECT [Id] - ,[Version] - ,[DocumentName] - ,[InvId] - ,[CustomerId] - ,[SendToAddress] - ,[SendToEmail] - ,[type] - ,[amount] - ,[amount_payed] - ,[subject] - ,[text] - ,[IsSent] - ,[IsFinal] - ,[CustomValues] - ,[DateSent] - ,[UserSent] - ,[DateFinalized] - ,[UserFinalized] - ,[DateCreated] - ,[UserCreated] - ,[DateModified] - ,[UserModified] - , [order] = ROW_NUMBER() OVER (ORDER BY DateFinalized ASC) - FROM [dbo].[fds__reminder] WHERE [invid] = @InvId AND [IsFinal] = 1; - - DECLARE @last varchar(1) = ISNULL(( SELECT TOP(1) [type] FROM @rem ORDER BY [DateFinalized] DESC), ''); - DECLARE @newnum int = CASE WHEN @last IN('l','f') THEN (SELECT COUNT(*) FROM @REM) + 1 - WHEN @last = 'm' THEN ISNULL( (SELECT TOP(1) TRY_CAST(LEFT([type],1) as int) FROM @rem ORDER BY [DateFinalized] DESC) , (SELECT COUNT(*) FROM @REM) + 1) - ELSE 1 - END; - - ---output - - SELECT - [type] = CAST( CASE WHEN @last = 'f' THEN 'm' WHEN @last = 'm' THEN 'l' ELSE 'f' END as varchar(1)) - ,[level] = @newnum; - - SELECT [subject], [amount], [amount_payed], [DateFinalized] FROM @REM; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__prepReminder.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__prepReminder.sql deleted file mode 100644 index 4d02e9a..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__prepReminder.sql +++ /dev/null @@ -1,86 +0,0 @@ - - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__prepReminder] - @InvId varchar(15) - , @authuser varchar(100) - , @type varchar(1) - , @level int -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_reminder', @authuser) < 2 - THROW 60000, N'not authorized', 1; - ELSE IF ISNULL(@type,'') NOT IN ('f','m','l') OR ISNULL(@level, 0) < 1 - THROW 60000, N'inputs not valid', 1; - ELSE IF NOT EXISTS (select 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId) AND NOT EXISTS (select 0 FROM [dbo].[mfr__invoices] WHERE [id] = TRY_CAST(@InvId as bigint)) - THROW 60000, N'invoice not found', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__prepReminder' ,@authuser , (SELECT * FROM (VALUES(@InvId, @authuser, @type, @level)) as z ([InvId],[authuser],[type],[level]) FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER)); - - - DECLARE @now datetime = GETDATE(); - DECLARE @today date = CAST(@now as date); - - - --output - - With inv1 as( - select TOP(1) * FROM [dbo].[fds__invoices] WHERE [id] = @InvId - ), inv as( - SELECT [Id] = cast([id] as varchar(15)) - ,[PaymentTerm] - ,[SendToemail] - ,[SendToAddress] - ,[CustomerId] - ,[Invoiceid] - ,[DateFinalized] - ,[InvoiceBalance] - FROM inv1 - UNION - SELECT [Id] = cast([id] as varchar(15)) - ,[PaymentTerm] = '10wd' - ,[SendToEmail] = sy.[SupportMail] - ,[SendToAddress] = (select TOP(1) sy.[name] + CHAR(10) + l.[addressString] + CHAR(10) + l.[postal] + ' ' + l.[city] + CHAR(10) + l.[country] from [dbo].[mfr__#locations] as l where l.[EntityId] = sy.[CustomerId]) - ,[CustomerId] - ,[Invoiceid] - ,[DateFinalized] = [DateOfCreation] - ,[InvoiceBalance] - FROM [dbo].[mfr__invoices] as mfri - LEFT JOIN (SELECT TOP(1) [invid] = p.[PartnerId], s.[CustomerId], cy.[supportmail], cy.[Name] - FROM dbo.[mfr__*PartnerSet] as p - JOIN [dbo].[mfr__servicerequests] as s on p.[EntityId] = s.[Id] AND p.[Property] = 'ServiceRequest:Invoices' - JOIN [dbo].[mfr__companies] as cy on s.[CustomerId] = cy.id - WHERE p.PartnerId = TRY_CAST(@invId as bigint)) as sy on mfri.Id = sy.[invid] - WHERE [id] = TRY_CAST(@invId as bigint) AND NOT EXISTS( SELECT 0 FROM inv1 ) --will only be used, if id not present in fds__invoices - AND [Filetype] not in ('PdfCancelInvoice') - AND [invoiceState] not in ('eIsCancelled','eIsPaid') - ) - SELECT TOP(1) - [today] = @today - , [invid] = inv.[id] - , [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf' - , [paymentterms] = [PaymentTerm] - , [invoiceemail] = [SendToemail] - , [invoiceaddress] = [SendToAddress] - , [CustomerId] = [CustomerId] - , [subject] = CASE WHEN @type = 'f' THEN N'Zahlungserinnerung' - WHEN @type = 'm' THEN FORMAT(@level,'0','de-de') + N'. Mahnung' - WHEN @type = 'l' THEN N'Letzte außergerichtliche Mahnung' - ELSE N'Zahlungserinnerung' - END - , [type] = @type - , [invoiceid] = inv.[Invoiceid] - , [invoicedate] = inv.[DateFinalized] - , [amount] = inv.[InvoiceBalance] - , [amount_payed] = CAST( ISNULL([dbo].[fds__fn_InvoicePaymentAmount](inv.[Id]),0.0) as numeric(10,2)) - FROM inv; - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__prepStorno_recreate.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__prepStorno_recreate.sql deleted file mode 100644 index f0c473a..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__prepStorno_recreate.sql +++ /dev/null @@ -1,459 +0,0 @@ - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__prepStorno_recreate] - @InvId varchar(20) - , @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__prepStorno_recreate' ,@authuser , ''); - - - - DECLARE @today date = GETDATE(); - DECLARE @type char(1) = 'r'; - - DECLARE @selReq as TABLE ([id] bigint); - IF EXISTS (SELECT 0 FROM [dbo].[fds__invoices] WHERE [id] = @InvId AND ISNULL([isfinal],0) = 1) - BEGIN - INSERT INTO @selReq SELECT DISTINCT irq.[mfr__servicerequest] from [dbo].[fds__invoice_servicerequests] as irq where InvId = @InvId; - END - ELSE IF EXISTS (SELECT 0 FROM [dbo].[mfr__invoices] as _i where TRY_CAST(_i.[Id] as varchar(20)) = @InvId AND [FileType] in ('PdfInvoice','PdfPartialInvoice')) - BEGIN - INSERT INTO @selReq SELECT DISTINCT ps.[PartnerId] from [dbo].[mfr__invoices] as _i JOIN [mfr__*PartnerSet] as ps ON _i.[Id] = ps.[EntityId] and ps.[Property] = 'Invoice:SourceServiceRequest' where TRY_CAST(_i.Id as varchar(20)) = @InvId and EXISTS ( SELECT 0 FROM [dbo].[mfr__servicerequests] as _s WHERE _s.[Id] = ps.[PartnerId]); - END - - DECLARE @SReq TABLE ( - [Id] [bigint] NULL, - [Name] [nvarchar](255) NULL, - [ExternalId] [nvarchar](255) NULL, - [InvoiceId] [nvarchar](255) NULL, - [ClosedAt] [datetime] NULL, - [ReleasedAt] [datetime] NULL, - [WorkDoneAt] [datetime] NULL, - [TargetTimeInMinutes] [bigint] NULL, - [DateModified] [datetime] NULL, - [DateOfCreation] [datetime] NULL, - [DueDateRangeStart] [datetime] NULL, - [DueDateRangeEnd] [datetime] NULL, - [PortalLink] [nvarchar](255) NULL, - [CostCenterId] [bigint] NULL, - [Description] [nvarchar](max) NULL, - [State] [nvarchar](255) NULL, - [CustomValues] [nvarchar](max) NULL, - [CurrentOwnerId] [bigint] NULL, - [CustomerId] [bigint] NULL, - [ParentServiceRequestId] [bigint] NULL, - [ParentExtenalId] [nvarchar](255) NULL, - [ParentWorkDoneAt] [date] NULL, - [ParentInvoiceId] [nvarchar](255) NULL, - [Location#ID] [bigint] NULL, - [Version] [int] NULL, - [IsTemplate] [bit] NULL, - [IsTemplateMobile] [bit] NULL, - [CreateFromServiceRequestTemplateId] [bigint] NULL, - [Type] [nvarchar](255) NULL, - [Summary] nvarchar(255) NULL, - [isChild] bit NULL, - [baseorder] int, - [provision] varchar(255) NULL - ); - - insert into @SReq - SELECT - [Id] - ,[Name] - ,[ExternalId] - ,[InvoiceId] - ,[ClosedAt] - ,[ReleasedAt] - ,[WorkDoneAt] - ,[TargetTimeInMinutes] - ,[DateModified] - ,[DateOfCreation] - ,[DueDateRangeStart] - ,[DueDateRangeEnd] - ,[PortalLink] - ,[CostCenterId] - ,[Description] - ,[State] - ,[CustomValues] - ,[CurrentOwnerId] - ,[CustomerId] - ,[ParentServiceRequestId] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then [id] ELSE s.[ParentServiceRequestId] END - ,[ParentExtenalId] = cast(CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then null ELSE '?' END as nvarchar(255)) - ,[ParentWorkDoneAt] = cast(NULL as datetime) - ,[ParentInvoiceId] = cast(NULL as nvarchar(255)) - ,[Location#ID] - ,[Version] - ,[IsTemplate] - ,[IsTemplateMobile] - ,[CreateFromServiceRequestTemplateId] - ,[Type] - ,[Summary] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[baseorder] = 0 - ,[provision] = NULL - FROM [dbo].[mfr__servicerequests] as s - WHERE [id] IN (SELECT [Id] from @selReq); - - IF @type <> 'i' -- for interim invoices, only one selected servicerequest is allowed -> childs and parents are ignored - BEGIN - INSERT INTO @SReq - SELECT s.[Id] - ,s.[Name] - ,s.[ExternalId] - ,s.[InvoiceId] - ,s.[ClosedAt] - ,s.[ReleasedAt] - ,s.[WorkDoneAt] - ,s.[TargetTimeInMinutes] - ,s.[DateModified] - ,s.[DateOfCreation] - ,s.[DueDateRangeStart] - ,s.[DueDateRangeEnd] - ,s.[PortalLink] - ,s.[CostCenterId] - ,s.[Description] - ,s.[State] - ,s.[CustomValues] - ,s.[CurrentOwnerId] - ,s.[CustomerId] - ,s.[ParentServiceRequestId] - ,[ParentExtenalId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[ExternalId] ELSE NULL END - ,[ParentWorkDoneAt] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[WorkDoneAt] ELSE NULL END - ,[ParentInvoiceId] = CASE WHEN s.[ParentServiceRequestId] = q.[id] THEN q.[InvoiceId] ELSE NULL END - ,s.[Location#ID] - ,s.[Version] - ,s.[IsTemplate] - ,s.[IsTemplateMobile] - ,s.[CreateFromServiceRequestTemplateId] - ,s.[Type] - ,[Summary] = NULL - ,[isChild] = CASE WHEN ISNULL(s.[ParentServiceRequestId],0) = 0 then 0 ELSE 1 END - ,[baseorder] = 0 - , [provision_start] = null - from [dbo].[mfr__servicerequests] as s JOIN @SReq as q ON (s.[ParentServiceRequestId] = q.[id] OR q.[ParentServiceRequestId] = s.[id]) --parents or childs - WHERE NOT EXISTS (SELECT * FROM @SReq as s2 WHERE s2.[id] = s.[Id]) - END; - - - - WITH q as ( - SELECT [id], [baseorder] = (ROW_NUMBER() OVER (ORDER BY ISNULL([ParentWorkDoneAt], [WorkDoneAt]), ISNULL([ParentServiceRequestId],[id])) * 100) - FROM @SReq as s - WHERE [ischild] = 0 - ) - UPDATE s SET - [baseorder] = q.baseorder - FROM @SReq as s JOIN q ON s.[id] = q.[id]; - - - WITH q as ( - SELECT s.[id] - , q.[ExternalId], q.[WorkDoneAt], q.[InvoiceId] - , [baseorder] = q.[baseorder] + ROW_NUMBER() OVER (PARTITION BY q.[id] ORDER BY s.[dateofcreation]) - FROM @SReq as s JOIN @SReq as q ON s.[ParentServiceRequestId] = q.[id] - WHERE s.[ischild] = 1 - ) - UPDATE s SET - [ParentExtenalId] = q.[ExternalId] - ,[ParentWorkDoneAt] = q.[WorkDoneAt] - ,[ParentInvoiceId] = q.[InvoiceId] - ,[baseorder] = q.[baseorder] - FROM @SReq as s JOIN q ON s.[Id] = q.[id]; - - IF @type <> 'i' - BEGIN - DELETE s FROM @SReq as s WHERE NOT EXISTS(SELECT * FROM @selReq as sel where sel.[id] = s.[Id]) - END - - - - DECLARE @step [dbo].[fds__tt__mfr_steps]; - DECLARE @itm [dbo].[fds__tt__mfr_items]; - DECLARE @company TABLE ( - [Id] [bigint] NOT NULL, - [Location#ID] [bigint] NULL, - [Version] [int] NOT NULL, - [IsPhysicalPerson] [bit] NOT NULL, - [IsOwner] [bit] NOT NULL, - [IsEmailInvoicingActive] [bit] NOT NULL, - [IsSupplier] [bit] NOT NULL, - [MappingId] [nvarchar](255) NULL, - [ExternalId] [nvarchar](255) NULL, - [Name] [nvarchar](255) NULL, - [Note] [nvarchar](255) NULL, - [SupportTelephone] [nvarchar](255) NULL, - [SupportFax] [nvarchar](255) NULL, - [SupportMail] [nvarchar](255) NULL, - [QuickSearch] [nvarchar](255) NULL, - [DateModified] [datetime] NOT NULL, - [CustomValues] [nvarchar](max) NOT NULL, - [MainContactId] [bigint] NOT NULL, - [address] nvarchar(1000) null - ); - INSERT INTO @company SELECT DISTINCT cy.*, [address] = [dbo].[fds__getCompanyAddress](cy.[id]) FROM [mfr__companies] as cy JOIN @SReq as s on cy.id = s.[CustomerId]; - DECLARE @companyids [dbo].[fds__tt__idlist]; - INSERT INTO @companyids SELECT DISTINCT [id] FROM @company; - - - INSERT INTO @itm - SELECT itm.*, [baseorder] = s.[baseorder], [order] = ROW_NUMBER() OVER (PARTITION BY [ServiceRequestId] ORDER BY [SortOrder], itm.[Id]) - from [dbo].[mfr__items] as itm JOIN @SReq as s ON itm.ServiceRequestId = s.[Id]; - - DECLARE @NUM_of_int_Invoices int = 0; - - - - -- Add virtual items for intermediate invoices - INSERT INTO @itm - SELECT - [Id] = -2 - ,[Version] = 0 - ,[QuantityHours] = 1 - ,[PlannedQuantityHours] = 1 - ,[ItemTypeId] = NULL - ,[ItemNumber] = NULL - ,[TrackingId] = NULL - ,[Manufacture] = NULL - ,[Price] = inv.InvoiceBalance_net * -1.0 --without VAT - ,[Costs] = inv.InvoiceBalance_net * -1.0 --without VAT - ,[NameOrNumber] = REPLACE(inv.DocumentName, '.pdf','') - ,[CustomValues] = NULL - ,[Note] = '

        ' + 'Anrechnung der ' + REPLACE(inv.DocumentName, '.pdf','') + ' vom ' + FORMAT(inv.[dateFinalized], 'dd.MM.yyyy', 'de-de') + '

        ' - ,[ExternalId] = NULL - ,[Discount] = NULL - ,[VAT] = inv.InvoiceVAT_1 - ,[IsManual] = 1 - ,[SortOrder] = ROW_NUMBER() OVER (PARTITION BY srq.[mfr__servicerequest] ORDER BY inv.[dateFinalized], inv.DateCreated) - ,[Type] = 'PartialInvoice' - ,[ServiceRequestId] = -2 --s.[id] - ,[ServiceObjectId] = Null - ,[CreatorId] = NULL - ,[UnitId] = 15825600513 - ,[UnitString] = 'Pauschal' - ,[baseorder] = s.[baseorder], [order] = 10000 + ROW_NUMBER() OVER (PARTITION BY srq.[mfr__servicerequest] ORDER BY inv.[dateFinalized], inv.DateCreated) - from [dbo].[fds__invoices] as inv - JOIN [dbo].[fds__invoice_servicerequests] as srq ON inv.[Id] = srq.[InvId] AND inv.[InvoiceType] = 'i' --important - JOIN @SReq as s ON srq.[mfr__servicerequest] = s.[Id] - WHERE inv.[InvoiceType] = 'i' --important; - AND ISNULL(inv.[isFinal],0) = 1 AND ISNULL(inv.[IsCanceled],0) = 0 - AND [dbo].[fds__fn_IntermediateIsAllocatedToOther](inv.[id],@InvId) = 0; --inonly those intermediates that are not already allocated to any other (than the to be replaced Inv); - - IF EXISTS (SELECT * FROM @itm WHERE [ServiceRequestId] = -2) - BEGIN - -- Add virtual request - INSERT INTO @SReq ([Id] - ,[Name] - ,[Type] - ,[isChild] - ,[baseorder]) - VALUES(-2 - ,'Abschlagszahlungen' - ,'vrii' - ,0 - ,999); - - SET @type = 'f'; - - END; - - - -- Add virtual item (to front) for (to-be) replaced invoice - INSERT INTO @itm - SELECT TOP(1) --can only be one - [Id] = -2 - ,[Version] = 0 - ,[QuantityHours] = 1 - ,[PlannedQuantityHours] = 1 - ,[ItemTypeId] = NULL - ,[ItemNumber] = NULL - ,[TrackingId] = NULL - ,[Manufacture] = NULL - ,[Price] = inv.InvoiceBalance_net * -1.0 --without VAT - ,[Costs] = inv.InvoiceBalance_net * -1.0 --without VAT - ,[NameOrNumber] = REPLACE(inv.DocumentName, '.pdf','') - ,[CustomValues] = NULL - ,[Note] = '

        ' + 'Storno der ' + REPLACE(inv.DocumentName, '.pdf','') + ' vom ' + FORMAT(inv.[dateFinalized], 'dd.MM.yyyy', 'de-de') + '

        ' - ,[ExternalId] = NULL - ,[Discount] = NULL - ,[VAT] = inv.InvoiceVAT_1 - ,[IsManual] = 1 - ,[SortOrder] = 1 - ,[Type] = 'CanceledInvoice' - ,[ServiceRequestId] = -3 --s.[id] - ,[ServiceObjectId] = Null - ,[CreatorId] = NULL - ,[UnitId] = 15825600513 - ,[UnitString] = 'Pauschal' - ,[baseorder] = 0, [order] = 1 - from [dbo].[fds__invoices] as inv - WHERE ISNULL(inv.[isFinal],0) = 1 AND ISNULL(inv.[IsCanceled],0) = 0 - AND inv.[Id] = @InvId; - - IF EXISTS (SELECT * FROM @itm WHERE [ServiceRequestId] = -3) - BEGIN - -- Add virtual request - INSERT INTO @SReq ([Id] - ,[Name] - ,[Type] - ,[isChild] - ,[baseorder]) - VALUES(-3 - ,'Storno' - ,'vrci' - ,0 - ,-999); - - END; - - - - - /** OUTPUT + get other types **/ - - - WITH app as( - SELECT [start] = MIN(CAST(CASE WHEN a.[StartDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[StartDateTime] as date) > s.[workdoneat] THEN CAST(a.[StartDateTime] as date) ELSE s.[workdoneat] END as date)) - , [end] = MAX(CAST(CASE WHEN a.[EndDateTime] is null THEN s.[workdoneat] WHEN CAST(a.[EndDateTime] as date) > s.[workdoneat] THEN CAST(a.[EndDateTime] as date) ELSE s.[workdoneat] END as date)) - , [CustomerId] = MIN (s.customerid) - FROM [dbo].[mfr__appointments] as a - RIGHT JOIN @SReq as s on a.[ServiceRequestId] = s.[Id] AND a.[State] in ('IsWorkDone') - ) - SELECT TOP(1) - [today] - , [provisionstart] = CASe WHEN @type = 'i' THEN NULL WHEN [start] = [end] THEN NULL ELSE [start] END - , [provisionend] = CASe WHEN @type = 'i' THEN @today ELSE [end] END -- Abschlagsrechnungen immer mit Tagesdatum - , [provisionlocation] = ISNULL((SELECT TOP(1) [provisionlocation] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([provisionlocation],'') <>''),N'') - , [sender] = N'Sebastian Fuchs GmbH & Co. KG ● Germaniastraße 15 ● 40223 Düsseldorf' - , [paymentterms] = ISNULL((SELECT TOP(1) [PaymentTerm] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([PaymentTerm],'') <>''),N'10wd') - , [invoiceemail] = ISNULL((SELECT TOP(1) [SendToemail] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([sendToEmail],'') <>''),(SELECT TOP(1) [SupportMail] FROM @company where IsEmailInvoicingActive = 1)) - , [invoiceaddress] = ISNULL((SELECT TOP(1) [SendToAddress] FROM [dbo].[fds__invoices] where [Id] = @InvId and ISNULL([SendToAddress],'') <>''),(SELECT TOP(1) CONCAT([name], CHAR(10), [address]) FROM @company ORDER BY IsEmailInvoicingActive DESC)) - , [tax_servicerefund] = 0.2 - , [CustomerId] = [CustomerId] - , [invoicetitle] = CASE WHEN @type = 'i' THEN (CASE WHEN @NUM_of_int_Invoices > 0 THEN CAST((@NUM_of_int_Invoices + 1) as varchar(3)) + '. ' ELSE '' END) + 'Abschlagsrechnung' - WHEN @type = 'f' THEN 'Schlussrechnung' WHEN @type = 'c' THEN 'Stornorechnung' ELSE 'Rechnung' END - , [type] = @type - FROM (VALUES(CAST(GETDATE() as date)))base ([today]) CROSS JOIN app; - - SELECT - * - ,[order] = ROW_NUMBER() OVER (ORDER BY [baseorder]) - FROM @SReq; - - - - IF @type <> 'i' - BEGIN - SELECT - itm.[Id] - , [net] = ISNULL([Price],0) - , [quantityhours] = ISNULL([quantityhours],1) - , itm.[UnitString] - , [net_val] = CAST( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) as numeric(7,2)) - , [bo_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * (1 + (ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) - , [vat] = CASE WHEN LOWER(itm.[type]) in ('Title','Text') THEN null ELSE FORMAT(ISNULL(vat.[vat], 19.0), '#0.0','de') + '%' END - , [vat_val] = CAST( ((ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0)) * ((ISNULL(vat.[vat], 19.0) * 0.01)) as numeric(7,2)) - , itm.[ServiceRequestId] - , itm.[SortOrder] - , itm.[Type] - , itm.[NameOrNumber] - , itm.[Note] - , [order] = (itm.[baseorder] * 100) + itm.[order] - FROM @itm as itm - LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'; - END - ELSE - BEGIN - WITH citm as ( - SELECT itm.[ServiceRequestId], [net_val] = CAST( SUM( (ISNULL([Price],0) * ISNULL([quantityhours],1)) - ISNULL([discount],0) ) as numeric(7,2)) - FROM @itm as itm - WHERE itm.[Type] = 'Material' - GROUP BY itm.[ServiceRequestId] - ) - SELECT - [Id] = -1 - , [net] = 1000.0 - , [quantityhours] = 1 - , [UnitString] = 'Pauschal' - , [net_val] = CAST( 1000.0 as numeric(7,2)) - , [bo_val] = CAST( 1000.0 * 1.19 as numeric(7,2)) - , [vat] = FORMAT( 19.0, '#0.0','de') + '%' - , [vat_val] = CAST( 1000.0 * 0.19 as numeric(7,2)) - , [ServiceRequestId] = s.[id] - , [SortOrder] = 0 - , [Type] = 'PartialInvoice' - , [NameOrNumber] = 'Abschlagsbetrag für bereits erbrachte Arbeiten, Dienstleistungen, Materiallieferungen und getätigte Bestellvorgänge zum oben genannten Bauvorhaben' - , [Note] = '' - , [order] = 1 - FROM @SReq as s; - - END - - - - INSERT INTO @step - SELECT - stp.* - , s.[baseorder] - , [order] = (s.[baseorder] * 100) + ROW_NUMBER() OVER (PARTITION BY stp.[ServiceRequestId] ORDER BY stp.[SortOrder], stp.[Id]) - FROM [dbo].[mfr__steps] as stp JOIN @SReq as s ON stp.ServiceRequestId = s.[Id]; - SELECT * from @step; - - - - SELECT - cc.[Id] - , [CompanyName] = cc.[name] - , ct.[Gender] - , ct.[FirstName] - , ct.[LastName] - , ct.[Email] - , cc.[address] - FROM [dbo].[fds__getCompanysContacts](@companyids) as ct join @company as cc on cc.[Id] = ct.[CyId]; - - - - SELECT DISTINCT - [ref] = 'Einsatzort ' + so.[name] - ,[address] = [dbo].[fds__fn_combineAddress] (so.[Name], loc.[AddressString], null - ,loc.[Postal] - ,loc.[City] - ,loc.[State] - ,loc.[Country]) - FROM @SReq as s - LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:ServiceObjects' and p.[EntityId] = s.[id] - LEFT JOIN [mfr__serviceobjects] as so ON so.id = p.[PartnerId] - LEFT JOIN [mfr__*PartnerSet] as po ON p.property = 'ServiceObject:Location' and po.[EntityId] = so.[Id] - LEFT join [dbo].[mfr__#locations] as loc ON (loc.[id] = so.[Location#ID] OR po.[PartnerId] = loc.[Id] OR (so.[Id] = loc.[EntityId] and loc.Property = 'ServiceObject:Location')) - WHERE so.id is not null and loc.[id] is not null - UNION - SELECT DISTINCT - [ref] = 'Auftragsort ' + [name] - ,[address] = [dbo].[fds__fn_combineAddress] (loc.[AddressString], null, null - ,loc.[Postal] - ,loc.[City] - ,loc.[State] - ,loc.[Country]) - FROM @SReq as s - LEFT JOIN [mfr__*PartnerSet] as p ON p.property = 'ServiceRequest:Location' and p.[EntityId] = s.[id] - join [dbo].[mfr__#locations] as loc ON (s.[Location#ID] = loc.[id] OR p.[PartnerId] = loc.[Id] OR (s.[Id] = loc.[EntityId] and loc.Property = 'ServiceRequest:Location')) - WHERE loc.[id] is not null - UNION - SELECT DISTINCT [ref] = 'Kunde' - , [address] - FROM @company; - - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByMonth.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByMonth.sql deleted file mode 100644 index b6d92b0..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByMonth.sql +++ /dev/null @@ -1,61 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceByMonth] - @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @today date = GETDATE() - DECLARE @tomorrow date = DATEADD(DAY,1,@today); - - IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - - DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable]; - - --Admin Table - INSERT INTO @Admin - VALUES ( - 'bbm' - ,'Umsatz nach Monaten' - ,'Umsatz nach Monaten' - ,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm') - ,'' - ,'table' --typ - , '' --settings - , '' - , 0) - - SELECT * FROM @Admin; - - WITH inv as ( - SELECT - i.[Dateofcreation] - ,i.[InvoiceBalance] - ,i.[InvoiceBalanceNetto] - FROM [dbo].[mfr__invoices] as i - where --i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and - i.[FileType] in( 'PdfInvoice','PdfCancelInvoice','PdfPartialInvoice') and ISNULL(InvoiceId,'') not in ('Preview','') - UNION - SELECT - [Dateofcreation] = i.[DateCreatEd] - ,i.[InvoiceBalance] - ,[InvoiceBalanceNetto] = i.[InvoiceBalance_net] - FROM [dbo].[fds__invoices] as i - WHERE i.[isFinal] = 1 - ) - SELECT - [Monat] = FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de') - , [∑ Umsätze] = FORMAT( SUM( ISNULL(inv.[InvoiceBalance],0) ), '#,0.00 €', 'de') - , [style:∑ Umsätze] = 'text-align: right' - , [∑ Umsätze_netto] = FORMAT( SUM( ISNULL(inv.[InvoiceBalanceNetto],0) ), '#,0.00 €', 'de') - , [style:∑ Umsätze_netto] = 'text-align: right' - FROM inv - GROUP BY FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de'); - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByYearTopCustomer.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByYearTopCustomer.sql deleted file mode 100644 index bf16db0..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByYearTopCustomer.sql +++ /dev/null @@ -1,91 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceByYearTopCustomer] - @year int, - - @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @today date = GETDATE() - DECLARE @tomorrow date = DATEADD(DAY,1,@today); - - IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable]; - - --Admin Table - INSERT INTO @Admin - VALUES ( - 'bbm' - ,'Umsatz der Top 10 Kunden für Jahr ' + CAST( @year as varchar(10)) - ,'Umsatz der Top 10 Kunden pro Jahr ' + CAST( @year as varchar(10)) - ,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm') - ,'' - ,'table' --typ - , '' --settings - , '' - , 0); - - SELECT * FROM @Admin; - - - WITH inv as( - select - [InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - ,[Dateofcreation] = CAST(_i.[Dateofcreation] as date) - ,srq.CustomerId - ,[customerName] = cy.Name - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[Dateofcreation] as date)) - FROM [dbo].[mfr__invoices] as _i - JOIN [dbo].[mfr__servicerequests] as srq on srq.InvoiceId like ('%' + _i.[InvoiceId] + '%') - JOIN [dbo].[mfr__companies] as cy on srq.CustomerId = cy.id - where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and YEAR(_i.[Dateofcreation]) = @year - GROUP BY CAST(_i.[Dateofcreation] as date), CustomerId, cy.name - UNION - select - [InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - ,[Dateofcreation] = CAST(_i.[DateCreated] as date) - ,_i.CustomerId - ,[customerName] = cy.Name - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[DateCreated] as date)) - FROM [dbo].[fds__invoices] as _i - JOIN [dbo].[mfr__companies] as cy on _i.CustomerId = cy.id - where YEAR(_i.[DateCreated]) = @year and _i.[isfinal] = 1 - GROUP BY CAST(_i.[DateCreated] as date), CustomerId, cy.name - ), yr1 as ( - SELECT - [year] = YEAR( inv.[Dateofcreation] ) - , [total] = SUM( ISNULL(inv.[InvoiceBalance],0) ) - FROM [dbo].[mfr__invoices] as inv - where inv.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') - GROUP BY YEAR( inv.[Dateofcreation]) - UNION - SELECT - [year] = YEAR( _i.[DateCreated] ) - , [total] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - FROM [dbo].[fds__invoices] as _i - WHERE _i.[isfinal] = 1 - GROUP BY YEAR( _i.[DateCreated]) - ), yr as ( - SELECT [year], [total] = SUM([total]) - from yr1 GROUP BY [year] - ) - SELECT TOP(10) - [Jahr] = CAST( YEAR( inv.[Dateofcreation] ) as varchar(4)) - , [Name d. Kunden] = [customerName] - , [∑ Umsätze] = FORMAT( SUM( ISNULL(inv.[InvoiceBalance],0) ), '#,0.00 €', 'de') - , [style:∑ Umsätze] = 'text-align: right' - , [% Anteil] = CASE WHEN yr.[total] > 0 THEN FORMAT( SUM( ISNULL(inv.[InvoiceBalance],0) ) / [total], '0 %', 'de') ELSE NULL END - , [style:% Anteil] = 'text-align: right' - , [order] = ROW_NUMBER() OVER ( ORDER By SUM( ISNULL(inv.[InvoiceBalance],0) ) DESC) - FROM inv LEFT JOIN yr on YEAR( inv.[Dateofcreation] ) = yr.[year] - GROUP BY YEAR( inv.[Dateofcreation] ), yr.[total], [customerName]; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByYearTopMaterial.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByYearTopMaterial.sql deleted file mode 100644 index 513a7dc..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceByYearTopMaterial.sql +++ /dev/null @@ -1,90 +0,0 @@ - - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceByYearTopMaterial] - @year int, - - @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @today date = GETDATE() - DECLARE @tomorrow date = DATEADD(DAY,1,@today); - - IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable]; - - --Admin Table - INSERT INTO @Admin - VALUES ( - 'bbm' - ,'Umsatz der Top 10 Materialien für Jahr ' + CAST( @year as varchar(10)) - ,'Umsatz der Top 10 Materialien für Jahr ' + CAST( @year as varchar(10)) - ,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm') - ,'' - ,'table' --typ - , '' --settings - , '' - , 0); - - SELECT * FROM @Admin; - - - WITH inv as( - select - [itmPrice] = SUM( (ISNULL(itm.[price],0) * ISNULL(itm.[QuantityHours],0) ) -ISNULL(itm.[discount],0) ) - ,[Dateofcreation] = CAST(_i.[Dateofcreation] as date) - ,[material] = itm.NameOrNumber - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[Dateofcreation] as date)) - FROM [dbo].[mfr__invoices] as _i - JOIN [dbo].[mfr__servicerequests] as srq on srq.InvoiceId like ('%' + _i.[InvoiceId] + '%') - JOIN [dbo].[mfr__items] as itm on srq.Id = itm.[ServiceRequestId] and itm.[Type] like 'Material' - where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and YEAR(_i.[Dateofcreation]) = @year - GROUP BY CAST(_i.[Dateofcreation] as date), itm.NameOrNumber - UNION - SELECT - [itmPrice] = SUM( ISNULL(itm.[value_total],0) ) - ,[Dateofcreation] = CAST(_i.[DateCreated] as date) - ,[material] = itm.[text] - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST(_i.[DateCreated] as date)) - FROM [dbo].[fds__invoices] as _i - JOIN [fds__invoice_items] as itm on itm.[invid] = _i.[id] AND itm.[Type] like 'Material' - where YEAR(_i.[DateCreated]) = @year and _i.[isfinal] = 1 - GROUP BY CAST(_i.[DateCreated] as date), itm.[text] - ), yr1 as ( - SELECT - [year] = YEAR( inv.[Dateofcreation] ) - , [total] = SUM( ISNULL(inv.[InvoiceBalance],0) ) - FROM [dbo].[mfr__invoices] as inv - where inv.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') - GROUP BY YEAR( inv.[Dateofcreation]) - UNION - SELECT - [year] = YEAR( _i.[DateCreated] ) - , [total] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - FROM [dbo].[fds__invoices] as _i - WHERE _i.[isfinal] = 1 - GROUP BY YEAR( _i.[DateCreated]) - ), yr as ( - SELECT [year], [total] = SUM([total]) - from yr1 GROUP BY [year] - ) - SELECT TOP(10) - [Jahr] = CAST( YEAR( inv.[Dateofcreation] ) as varchar(4)) - , [Material] = [material] - , [∑ Umsätze] = FORMAT( SUM( ISNULL(inv.[itmPrice],0) ), '#,0.00 €', 'de') - , [style:∑ Umsätze] = 'text-align: right' - , [% Anteil] = CASE WHEN yr.[total] > 0 THEN FORMAT( SUM( ISNULL(inv.[itmPrice],0) ) / [total], '0 %', 'de') ELSE NULL END - , [style:% Anteil] = 'text-align: right' - , [order] = ROW_NUMBER() OVER ( ORDER By SUM( ISNULL(inv.[itmPrice],0) ) DESC) - FROM inv LEFT JOIN yr on YEAR( inv.[Dateofcreation] ) = yr.[year] - GROUP BY YEAR( inv.[Dateofcreation] ), yr.[total], [material]; - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceTrendByMonth.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceTrendByMonth.sql deleted file mode 100644 index f94b85a..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceTrendByMonth.sql +++ /dev/null @@ -1,72 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceTrendByMonth] - @year int - ,@month int, - - @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - - - DECLARE @today date = GETDATE() - DECLARE @tomorrow date = DATEADD(DAY,1,@today); - - IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - set @month = ISNULL(@month, Month(@today)); - set @year= ISNULL(@year, Year(@year)); - - - DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable]; - - --Admin Table - INSERT INTO @Admin - VALUES ( - 'bbm' - ,'UmsatzTrend für Monat ' + FORMAT(@Month, '00') + ' / ' + FORMAT(@year, '0000') - ,'UmsatzTrend für Monat ' + FORMAT(@Month, '00') + ' / ' + FORMAT(@year, '0000') - ,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm') - ,'' - ,'chart' --typ - , '{ "charttype": "line", "x1_label_angle": -90, "color": "e0301e", "linewidth": 2, "valuelabels": false, "marker": "none", "colorpalette": "00B050;00B050;00B050", "x1_column": "date", "y1_column": "Value", "y1_title": "Actuals", "x1_title": "", "series_column": "series", "legend_position": "bottom", "show_datalabel": true, "datalabelfontsizescale": 0.6, "height": 600, "width": 1200, "x1_labelformat": "ddd, dd/MM"}' --settings - , '' - , 0) - - SELECT * FROM @Admin; - - - - - with inv as( - select - [InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - ,[Dateofcreation] = CAST([Dateofcreation] as date) - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Dateofcreation] as date)) - FROM [dbo].[mfr__invoices] as _i - where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and month([Dateofcreation]) = @Month and YEAR([Dateofcreation]) = @year - GROUP BY CAST([Dateofcreation] as date) - UNION - select - [InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - ,[Dateofcreation] = CAST([Datecreated] as date) - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Datecreated] as date)) - FROM [dbo].[fds__invoices] as _i where _i.IsFinal = 1 and month([Datecreated]) = @Month and YEAR([Datecreated]) = @year - GROUP BY CAST([Datecreated] as date) - ) - SELECT - --[Monat] = FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de') - --,[Umsatz] = FORMAT( ISNULL(inv.[InvoiceBalance],0) , '#0.00', 'de') - [series] = 'Umsatz' - ,[date] = CAST(inv.[Dateofcreation] as date) - ,[value] = CAST( SUM( ISNULL(inv.[InvoiceBalance],0) ) OVER (ORDER BY [seq]) as numeric(9,2)) - FROM inv - ORDER BY [seq] - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceTrendByYear.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceTrendByYear.sql deleted file mode 100644 index 744caf0..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__r_getBalanceTrendByYear.sql +++ /dev/null @@ -1,71 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__r_getBalanceTrendByYear] - @year int, - - @authuser varchar(100) -AS -BEGIN - SET NOCOUNT ON; - - - - DECLARE @today date = GETDATE() - DECLARE @tomorrow date = DATEADD(DAY,1,@today); - - IF [dbo].[fis_getModuleAuth]('fds_reports', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - set @year= ISNULL(@year, Year(@year)); - - - DECLARE @Admin [dbo].[fds__tt__admin_ReportAdminTable]; - - --Admin Table - INSERT INTO @Admin - VALUES ( - 'bbm' - ,'UmsatzTrend für Jahr ' + FORMAT(@year, '0000') - ,'UmsatzTrend für Jahr ' + FORMAT(@year, '0000') - ,'' + FORMAT(GETDATE(), 'yyyy-MM-dd HH:mm') - ,'' - ,'chart' --typ - , '{ "charttype": "line", "x1_label_angle": -90, "color": "e0301e", "linewidth": 2, "valuelabels": false, "marker": "none", "colorpalette": "00B050;00B050;00B050", "x1_column": "date", "y1_column": "Value", "y1_title": "Actuals", "x1_title": "", "series_column": "series", "legend_position": "bottom", "show_datalabel": true, "datalabelfontsizescale": 0.6, "height": 600, "width": 1200, "x1_labelformat": "ddd, dd/MM"}' --settings - , '' - , 0) - - SELECT * FROM @Admin; - - - - - with inv as( - select - [InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - ,[Dateofcreation] = CAST([Dateofcreation] as date) - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Dateofcreation] as date)) - FROM [dbo].[mfr__invoices] as _i - where _i.[invoicestate] in('eIsSent','eIsPaid','eIsOpen') and YEAR([Dateofcreation]) = @year - GROUP BY CAST([Dateofcreation] as date) - UNION - select - [InvoiceBalance] = SUM( ISNULL(_i.[InvoiceBalance],0) ) - ,[Dateofcreation] = CAST([Datecreated] as date) - ,[seq] = ROW_NUMBER() OVER (ORDER BY CAST([Datecreated] as date)) - FROM [dbo].[fds__invoices] as _i where _i.IsFinal = 1 and YEAR([Datecreated]) = @year - GROUP BY CAST([Datecreated] as date) - ) - SELECT - --[Monat] = FORMAT( inv.[Dateofcreation], 'yyyy - MM', 'de') - --,[Umsatz] = FORMAT( ISNULL(inv.[InvoiceBalance],0) , '#0.00', 'de') - [series] = 'Umsatz' - ,[date] = CAST(inv.[Dateofcreation] as date) - ,[value] = CAST( SUM( ISNULL(inv.[InvoiceBalance],0) ) OVER (ORDER BY [seq]) as numeric(9,2)) - FROM inv - ORDER BY [seq] - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__setBankingtransaction_autoAssigns.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__setBankingtransaction_autoAssigns.sql deleted file mode 100644 index 65a504f..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__setBankingtransaction_autoAssigns.sql +++ /dev/null @@ -1,70 +0,0 @@ - --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__setBankingtransaction_autoAssigns] -AS -BEGIN - SET NOCOUNT ON; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__setBankingtransaction_autoAssigns' ,'', ''); - - with - icr as ( - SELECT * FROM [dbo].[fds__getInvoiceCredits] (null, null) - ), ic as ( - SELECT - icr.[uid] - , icr.[InvoiceId] - , [InvID] = CAST(i.[Id] as varchar(15)) - , icr.amount - , [auto] = CAST(1 as bit) - FROM icr - JOIN [dbo].[fds__invoices] as i on icr.[InvoiceId] = i.[InvoiceId] and i.[InvoiceId] is not null and i.[IsFinal] = 1 - UNION - SELECT - icr.[uid] - , icr.[InvoiceId] - , [InvID] = CAST(i.[Id] as varchar(15)) - , icr.amount - , [auto] = CAST(1 as bit) - FROM icr - JOIN [dbo].[mfr__invoices] as i on icr.[InvoiceId] = i.[InvoiceId] and i.[InvoiceId] is not null - --UNION - --SELECT - -- bt.[uid] - -- , i.[InvoiceId] - -- , [InvID] = i.[Id] - -- , bt.[amount] - -- , [auto] = CAST(0 as bit) - --FROM [dbo].[fds__bankingtransactions] as bt JOIN [dbo].[fds__bankingtransactions_settings] as bs ON bt.uid = bs.[banking_uid] AND ISNULL(bs.[assigned_invoice_id], '') <> '' - -- JOIN [dbo].[fds__invoices] as i on bs.[assigned_invoice_id] = i.[Id] and i.[IsFinal] = 1 - - ), ic2 as ( - SELECT [banking_uid] = ic.[uid] - , [amount] = SUM(ISNULL(ic.[amount], 0.0)) - , [InvID_csv] = STRING_AGG(ic.[InvID], ',') - from ic - WHERE ic.[InvID] is not null - GROUP BY ic.[uid] - ) - --select * from ic2 - MERGE [dbo].[fds__bankingtransactions_settings] as TARGET - USING ic2 as SOURCE ON TARGET.[banking_uid] = SOURCE.[banking_uid] - WHEN NOT MATCHED BY TARGET THEN - INSERT - ([banking_uid] - ,[auto_invoice_id]) - VALUES - (SOURCE.[banking_uid] - ,SOURCE.[InvID_csv]) - WHEN MATCHED THEN - UPDATE SET [auto_invoice_id] = [dbo].[ott_merge_csv](TARGET.[auto_invoice_id],SOURCE.[InvID_csv]); - - - - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/backup__fds__setInvoicePayed.sql b/Fuchs_Database/dbo/Stored Procedures/backup__fds__setInvoicePayed.sql deleted file mode 100644 index ad0d013..0000000 --- a/Fuchs_Database/dbo/Stored Procedures/backup__fds__setInvoicePayed.sql +++ /dev/null @@ -1,46 +0,0 @@ --- ============================================= --- Author: --- Create date: --- Description: --- ============================================= -CREATE PROCEDURE [dbo].[backup__fds__setInvoicePayed] - @Id varchar(15), - @authuser varchar(25) -AS -BEGIN - SET NOCOUNT ON; - DECLARE @now datetime = GETUTCDATE(); - - IF [dbo].[fis_getModuleAuth]('fds_inv', @authuser) < 2 - THROW 60000, N'not authorized', 1; - - INSERT INTO [dbo].[fds__admin_activity] ([activity] ,[authuser] ,[info]) - VALUES ('fds__setInvoicePayed' ,@authuser , (SELECT * FROM (VALUES(@Id, @authuser)) as z ([id],[authuser]) FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER)); - - UPDATE [dbo].[fds__invoices] SET [IsPayed] = 1, [paymentstatus] = 'm', [DateModified] = @now, [UserModified] = @authuser, [version] = (ISNULL([version],0) + 1) - OUTPUT inserted.* - WHERE [Id] = @Id AND @Id is not null and [IsPayed] = 0; - - IF TRY_CAST(@id as bigint) is not null and EXISTS(SELECT 0 FROM [dbo].[mfr__invoices] as i where i.[id] = TRY_CAST(@id as bigint)) - BEGIN - MERGE [dbo].[fds__custom_invoiceinfo] as TARGET - USING (VALUES - (TRY_CAST(@id as bigint) - ,CAST('m' as char(1)) - ,CAST(1 as bit))) as SOURCE([InvId],[PaymentStatus],[isPayed]) - ON TARGET.[InvID] = SOURCE.[InvID] - WHEN MATCHED THEN - UPDATE SET TARGET.[PaymentStatus] = SOURCE.[PaymentStatus] - ,TARGET.[isPayed] = SOURCE.[isPayed] - WHEN NOT MATCHED THEN - INSERT ([InvId],[PaymentStatus],[isPayed],[file]) - VALUES( - SOURCE.[InvId] - ,SOURCE.[PaymentStatus] - ,SOURCE.[isPayed] - ,NULL - ); - - END - -END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__createReminder.sql b/Fuchs_Database/dbo/Stored Procedures/fds__createReminder.sql index c3618b4..dac340b 100644 --- a/Fuchs_Database/dbo/Stored Procedures/fds__createReminder.sql +++ b/Fuchs_Database/dbo/Stored Procedures/fds__createReminder.sql @@ -57,7 +57,8 @@ BEGIN ,[UserCreated] ,[DateModified] ,[UserModified] - ,[file]) + ,[file] + ,[file_guid]) OUTPUT inserted.* INTO @OUT VALUES @@ -82,7 +83,8 @@ BEGIN ,@authuser ,@now ,@authuser - ,NULL); + ,NULL + ,NEWID()); SELECT TOP(1) @Id = [id] FROM @out; diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransaction_manual.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransaction_manual.sql new file mode 100644 index 0000000..3a53a7f --- /dev/null +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransaction_manual.sql @@ -0,0 +1,24 @@ +-- ============================================= +-- Description: Returns the raw field values of a single manually entered +-- banking transaction (TransactionTypeIdCode = 'MAN'), so the +-- "manuelle Buchung ändern" dialog can be pre-filled. +-- ============================================= +CREATE PROCEDURE [dbo].[fds__getBankingtransaction_manual] + @taID varchar(10), + @authuser varchar(100) +AS +BEGIN + SET NOCOUNT ON; + IF [dbo].[fis_getModuleAuth]('fds_bam', @authuser) < 1 + THROW 60000, N'not authorized', 1; + + SELECT + [taID] + , [ValueDate] + , [Amount] + , [NameOfPayer] + , [SepaRemittanceInformation] + FROM [dbo].[fds__bankingtransactions] + WHERE [taID] = @taID + AND [TransactionTypeIdCode] = 'MAN'; +END diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransaction_manual_1.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransaction_manual_1.sql new file mode 100644 index 0000000..aa83529 --- /dev/null +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransaction_manual_1.sql @@ -0,0 +1,25 @@ + +-- ============================================= +-- Description: Returns the raw field values of a single manually entered +-- banking transaction (TransactionTypeIdCode = 'MAN'), so the +-- "manuelle Buchung ändern" dialog can be pre-filled. +-- ============================================= +CREATE PROCEDURE [dbo].[fds__getBankingtransaction_manual] + @taID varchar(10), + @authuser varchar(100) +AS +BEGIN + SET NOCOUNT ON; + IF [dbo].[fis_getModuleAuth]('fds_bam', @authuser) < 1 + THROW 60000, N'not authorized', 1; + + SELECT + [taID] + , [ValueDate] + , [Amount] + , [NameOfPayer] + , [SepaRemittanceInformation] + FROM [dbo].[fds__bankingtransactions] + WHERE [taID] = @taID + AND [TransactionTypeIdCode] = 'MAN'; +END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransfers_questionable.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransfers_questionable.sql index d935289..21b8aad 100644 --- a/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransfers_questionable.sql +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getBankingtransfers_questionable.sql @@ -54,6 +54,7 @@ BEGIN , bt.NameOfPayer , bt.SepaRemittanceInformation , bt.EndToEndReference + , [IsManual] = CAST(CASE WHEN bt.[TransactionTypeIdCode] = 'MAN' THEN 1 ELSE 0 END AS bit) , [order] = ROW_NUMBER() OVER (ORDER BY bt.[valuedate] DESC) FROM dbo.[fds__bankingtransactions] as bt @@ -67,7 +68,7 @@ BEGIN AND NOT (ISNULL(bt.[AccountNumberOfPayer],'') IN ('DE52301502000002091478') AND ISNULL(bt.[SepaRemittanceInformation],'') like ('%umbuchung%')) AND NOT (ISNULL(bt.[AccountNumberOfPayer],'') IN ('DE23501108006161606386') AND ISNULL(bt.[SepaRemittanceInformation],'') like ('%ebay%')) AND NOT (ISNULL(bt.[SepaRemittanceInformation],'') like ('%Umsatzsteuer%')) - AND ISNULL(bt.AccountNumberOfPayer ,'') <> '' + AND (ISNULL(bt.AccountNumberOfPayer ,'') <> '' OR bt.[TransactionTypeIdCode] = 'MAN') ) AND CASE WHEN inv.[InvId] is null THEN 1 WHEN (ISNULL(inv.ispayed,0) = 1 and fds = 1) THEN 0 diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getReminder.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getReminder.sql index 3d726f0..93fc9fb 100644 --- a/Fuchs_Database/dbo/Stored Procedures/fds__getReminder.sql +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getReminder.sql @@ -44,6 +44,7 @@ BEGIN ,[DateModified] ,[UserModified] ,[file] = CASE WHEN ISNULL(@includefile,0) = 1 THEN [file] else NULL END -- do not return file if not explicitly requested + ,[file_guid] FROM [dbo].[fds__reminder] WHERE [Id] = @Id AND @Id is not null; diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__prepInvoice.sql b/Fuchs_Database/dbo/Stored Procedures/fds__prepInvoice.sql index 82dae8d..7ce1154 100644 --- a/Fuchs_Database/dbo/Stored Procedures/fds__prepInvoice.sql +++ b/Fuchs_Database/dbo/Stored Procedures/fds__prepInvoice.sql @@ -177,7 +177,7 @@ BEGIN DECLARE @step [dbo].[fds__tt__mfr_steps]; - DECLARE @itm [dbo].[fds__tt__mfr_items]; + DECLARE @itm [dbo].[fds__tt__mfr_items+]; DECLARE @company TABLE ( [Id] [bigint] NOT NULL, [Location#ID] [bigint] NULL, @@ -207,9 +207,17 @@ BEGIN INSERT INTO @itm SELECT itm.* - , [baseorder] = s.[baseorder], [order] = ROW_NUMBER() OVER (PARTITION BY [ServiceRequestId] ORDER BY [SortOrder], itm.[Id]) - from [dbo].[mfr__items] as itm JOIN @SReq as s ON itm.ServiceRequestId = s.[Id] - where itm.[Type] <> 'PartialInvoice'; + , [baseorder] = s.[baseorder] + , [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id]) + , [SetItmID] = MAX(CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) = 0 THEN itm.[Id] ELSE NULL END) + OVER ( + PARTITION BY itm.[ServiceRequestId] + ORDER BY itm.[SortOrder], itm.[Id] + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) + FROM [dbo].[mfr__items] as itm + JOIN @SReq as s ON itm.ServiceRequestId = s.[Id] + WHERE itm.[Type] <> 'PartialInvoice'; DECLARE @NUM_of_int_Invoices int = 0; @@ -264,6 +272,7 @@ BEGIN ,[UnitString] = 'Pauschal' ,[baseorder] = ai.[baseorder] , [order] = 10000 + (ROW_NUMBER() OVER (PARTITION BY ai.[mfr__servicerequest] ORDER BY ai.[dateFinalized], ai.DateCreated) * 10) + ,[SetItmId] = null FROM associated_interims as ai UNION SELECT @@ -293,6 +302,7 @@ BEGIN ,[UnitString] = 'Pauschal' ,[baseorder] = ai.[baseorder] , [order] = 10001 + (ROW_NUMBER() OVER (PARTITION BY ai.[mfr__servicerequest] ORDER BY ai.[dateFinalized], ai.DateCreated) * 10) + ,[SetItmId] = null FROM associated_interims as ai WHERE ISNULL(ai.open_amount,0.0) > 0.0; @@ -381,6 +391,7 @@ BEGIN , itm.[NameOrNumber] , [Note] = IIF(ISNULL([Discount], 0) > 0, 'Rabatt '+ FORMAT([discount]*0.01,'#.0%','de-de') + '', '') + ISNULL(itm.[Note],'') , [order] = (itm.[baseorder] * 100) + itm.[order] + , [SetItmId] FROM @itm as itm LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'; END diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__prepStorno_recreate.sql b/Fuchs_Database/dbo/Stored Procedures/fds__prepStorno_recreate.sql index 995934b..c1a427d 100644 --- a/Fuchs_Database/dbo/Stored Procedures/fds__prepStorno_recreate.sql +++ b/Fuchs_Database/dbo/Stored Procedures/fds__prepStorno_recreate.sql @@ -176,7 +176,7 @@ BEGIN DECLARE @step [dbo].[fds__tt__mfr_steps]; - DECLARE @itm [dbo].[fds__tt__mfr_items]; + DECLARE @itm [dbo].[fds__tt__mfr_items+]; DECLARE @company TABLE ( [Id] [bigint] NOT NULL, [Location#ID] [bigint] NULL, @@ -204,8 +204,17 @@ BEGIN INSERT INTO @itm - SELECT itm.*, [baseorder] = s.[baseorder], [order] = ROW_NUMBER() OVER (PARTITION BY [ServiceRequestId] ORDER BY [SortOrder], itm.[Id]) - from [dbo].[mfr__items] as itm JOIN @SReq as s ON itm.ServiceRequestId = s.[Id]; + SELECT itm.* + , [baseorder] = s.[baseorder] + , [order] = ROW_NUMBER() OVER (PARTITION BY itm.[ServiceRequestId] ORDER BY itm.[SortOrder], itm.[Id]) + , [SetItmID] = MAX(CASE WHEN itm.[Type] = 'Set' AND ISNULL(itm.[Price], 0) = 0 THEN itm.[Id] ELSE NULL END) + OVER ( + PARTITION BY itm.[ServiceRequestId] + ORDER BY itm.[SortOrder], itm.[Id] + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + ) + FROM [dbo].[mfr__items] as itm + JOIN @SReq as s ON itm.ServiceRequestId = s.[Id]; DECLARE @NUM_of_int_Invoices int = 0; @@ -238,7 +247,9 @@ BEGIN ,[CreatorId] = NULL ,[UnitId] = 15825600513 ,[UnitString] = 'Pauschal' - ,[baseorder] = s.[baseorder], [order] = 10000 + ROW_NUMBER() OVER (PARTITION BY srq.[mfr__servicerequest] ORDER BY inv.[dateFinalized], inv.DateCreated) + ,[baseorder] = s.[baseorder] + , [order] = 10000 + ROW_NUMBER() OVER (PARTITION BY srq.[mfr__servicerequest] ORDER BY inv.[dateFinalized], inv.DateCreated) + ,[SetItmID] = null from [dbo].[fds__invoices] as inv JOIN [dbo].[fds__invoice_servicerequests] as srq ON inv.[Id] = srq.[InvId] AND inv.[InvoiceType] = 'i' --important JOIN @SReq as s ON srq.[mfr__servicerequest] = s.[Id] @@ -292,7 +303,9 @@ BEGIN ,[CreatorId] = NULL ,[UnitId] = 15825600513 ,[UnitString] = 'Pauschal' - ,[baseorder] = 0, [order] = 1 + ,[baseorder] = 0 + , [order] = 1 + ,[SetItmID] = null from [dbo].[fds__invoices] as inv WHERE ISNULL(inv.[isFinal],0) = 1 AND ISNULL(inv.[IsCanceled],0) = 0 AND inv.[Id] = @InvId; @@ -373,6 +386,7 @@ BEGIN , itm.[NameOrNumber] , itm.[Note] , [order] = (itm.[baseorder] * 100) + itm.[order] + , [SetItmId] FROM @itm as itm LEFT JOIN [dbo].[fds__custom_vat] as vat ON itm.[Id] = vat.[EntityId] and vat.[EntityType] = 'item'; END diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__setBankingtransaction_manual.sql b/Fuchs_Database/dbo/Stored Procedures/fds__setBankingtransaction_manual.sql new file mode 100644 index 0000000..37f4d96 --- /dev/null +++ b/Fuchs_Database/dbo/Stored Procedures/fds__setBankingtransaction_manual.sql @@ -0,0 +1,64 @@ + +CREATE PROCEDURE [dbo].[fds__setBankingtransaction_manual] + @taID varchar(10) = NULL, -- NULL=INSERT, value=UPDATE, value+others NULL=DELETE + @ValueDate date = NULL, + @Amount numeric(9,2) = NULL, + @NameOfPayer nvarchar(140) = NULL, + @SepaRemittanceInformation varchar(200) = NULL, + @authuser varchar(100) = NULL +AS +BEGIN + SET NOCOUNT ON; + IF [dbo].[fis_getModuleAuth]('fds_bam', @authuser) < 2 + THROW 60000, N'not authorized', 1; + + BEGIN TRY + + -- DELETE + IF @taID IS NOT NULL AND @Amount IS NULL + BEGIN + DELETE FROM [dbo].[fds__bankingtransactions] + WHERE [taID] = @taID; + RETURN; + END + + -- INSERT + IF @taID IS NULL + BEGIN + INSERT INTO [dbo].[fds__bankingtransactions] + ([taID], [AccountIdentification], [ValueDate], [Amount], + [FundsCode], [AccountNumberOfPayer], [BankCodeOfPayer], + [CompensationAmount], [CreditorReference], [CreditorsReferenceParty], + [CustomerReference], [EndToEndReference], [JournalNumber], + [MandateReference], [NameOfPayer], [OriginalAmount], + [OriginatorsIdentificationCode], [PayersReferenceParty], + [PostingText], [SepaRemittanceInformation], [TextKeyAddition], + [TransactionCode], [IsUnstructuredData], [UnstructuredData], + [UnstructuredRemittanceInformation], [DebitCreditMark], [TransactionTypeIdCode]) + VALUES + ([dbo].[fds__fn_bankingtransaction_id](), '999', @ValueDate, @Amount, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, NULL, NULL, + NULL, @NameOfPayer, NULL, + NULL, NULL, + 'MANUELLE GUTSCHRIFT', @SepaRemittanceInformation, NULL, + NULL, 0, NULL, + NULL, 'C', 'MAN'); + RETURN; + END + + -- UPDATE + UPDATE [dbo].[fds__bankingtransactions] + SET + [ValueDate] = @ValueDate, + [Amount] = @Amount, + [NameOfPayer] = @NameOfPayer, + [SepaRemittanceInformation] = @SepaRemittanceInformation + WHERE [taID] = @taID; + + END TRY + BEGIN CATCH + THROW; + END CATCH +END \ No newline at end of file diff --git a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__companies.sql b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__companies.sql index 965d0df..812a79b 100644 --- a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__companies.sql +++ b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__companies.sql @@ -20,19 +20,19 @@ BEGIN IF @referencetable = 'mfr__companies' BEGIN DELETE p - OUTput deleted.*, @now INTO [dbo].[mfr__d_*PartnerSet] + --OUTput deleted.*, @now INTO [dbo].[mfr__d_*PartnerSet] FROM [dbo].[mfr__*PartnerSet] as p WHERE p.Property like 'Company[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = p.[EntityId]); DELETE cv - OUTput deleted.*, @now INTO [dbo].[mfr__d_#customvalues] + --OUTput deleted.*, @now INTO [dbo].[mfr__d_#customvalues] FROM [dbo].[mfr__#customvalues] as cv WHERE cv.Property like 'Company[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = cv.[EntityId]); DELETE lc - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_#locations] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_#locations] FROM [dbo].[mfr__#locations] as lc WHERE lc.Property like 'Company[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = lc.[EntityId]); DELETE cm - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_comments] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_comments] FROM [dbo].[mfr__comments] as cm WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = cm.[CompanyId] AND ISNULL(cm.[ServiceRequestId],0) = 0); END diff --git a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__serviceobjects.sql b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__serviceobjects.sql index 5dffeb8..87ca13f 100644 --- a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__serviceobjects.sql +++ b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__serviceobjects.sql @@ -20,19 +20,19 @@ BEGIN IF @referencetable = 'mfr__serviceobjects' BEGIN DELETE p - OUTput deleted.*, @now INTO [dbo].[mfr__d_*PartnerSet] + --OUTput deleted.*, @now INTO [dbo].[mfr__d_*PartnerSet] FROM [dbo].[mfr__*PartnerSet] as p WHERE p.Property like 'ServiceObject[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = p.[EntityId]); DELETE cv - OUTput deleted.*, @now INTO [dbo].[mfr__d_#customvalues] + --OUTput deleted.*, @now INTO [dbo].[mfr__d_#customvalues] FROM [dbo].[mfr__#customvalues] as cv WHERE cv.Property like 'ServiceObject[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = cv.[EntityId]); DELETE lc - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_#locations] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_#locations] FROM [dbo].[mfr__#locations] as lc WHERE lc.Property like 'ServiceObject[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = lc.[EntityId]); DELETE itm - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_items] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_items] FROM [dbo].[mfr__items] as itm WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = itm.[ServiceObjectId] and ISNULL(itm.[ServiceRequestId],0) = 0); END diff --git a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__servicerequests.sql b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__servicerequests.sql index 0816004..564c525 100644 --- a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__servicerequests.sql +++ b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__servicerequests.sql @@ -24,39 +24,39 @@ BEGIN IF @referencetable = 'mfr__servicerequests' BEGIN DELETE p - OUTput deleted.*, @now INTO [dbo].[mfr__d_*PartnerSet] + --OUTput deleted.*, @now INTO [dbo].[mfr__d_*PartnerSet] FROM [dbo].[mfr__*PartnerSet] as p WHERE p.Property like 'ServiceRequest[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = p.[EntityId]); DELETE cv - OUTput deleted.*, @now INTO [dbo].[mfr__d_#customvalues] + --OUTput deleted.*, @now INTO [dbo].[mfr__d_#customvalues] FROM [dbo].[mfr__#customvalues] as cv WHERE cv.Property like 'ServiceRequest[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = cv.[EntityId]); DELETE lc - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_#locations] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_#locations] FROM [dbo].[mfr__#locations] as lc WHERE lc.Property like 'ServiceRequest[:]%' AND EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = lc.[EntityId]); DELETE stp - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_steps] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_steps] FROM [dbo].[mfr__steps] as stp WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = stp.[ServiceRequestId] and ISNULL(stp.[StepListTemplateId],0) = 0); DELETE app - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_appointments] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_appointments] FROM [dbo].[mfr__appointments] as app WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = app.[ServiceRequestId]); DELETE itm - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_items] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_items] FROM [dbo].[mfr__items] as itm WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = itm.[ServiceRequestId] and ISNULL(itm.[ServiceobjectId],0) = 0); DELETE te - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_timeevents] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_timeevents] FROM [dbo].[mfr__timeevents] as te WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = te.[ServiceRequestId]); DELETE sm - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_stockmovements] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_stockmovements] FROM [dbo].[mfr__stockmovements] as sm WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = sm.[ServiceRequestId]); DELETE cm - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_comments] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_comments] FROM [dbo].[mfr__comments] as cm WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = cm.[ServiceRequestId] AND ISNULL(cm.[CompanyId],0) = 0); END diff --git a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steplisttemplates.sql b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steplisttemplates.sql index eff731b..272e8e8 100644 --- a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steplisttemplates.sql +++ b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steplisttemplates.sql @@ -22,7 +22,7 @@ BEGIN IF @referencetable = 'mfr__steplisttemplates' BEGIN DELETE stp - OUTPUT deleted.*, @now INTO [dbo].[mfr__d_steps] + --OUTPUT deleted.*, @now INTO [dbo].[mfr__d_steps] FROM [dbo].[mfr__steps] as stp WHERE EXISTS (SELECT * FROM @tmp as tmp where tmp.[id] = stp.[StepListTemplateId] and ISNULL(stp.[ServiceRequestId],0) = 0); END diff --git a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steps.sql b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steps.sql index 3502e29..c8a6157 100644 --- a/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steps.sql +++ b/Fuchs_Database/dbo/Stored Procedures/mfr__updt__steps.sql @@ -124,26 +124,26 @@ BEGIN $action, deleted.* INTO @ARCHIVE; - INSERT INTO [dbo].[mfr__d_steps] - SELECT [Id] - ,[MobileId] - ,[Version] - ,[Name] - ,[IsDone] - ,[HasError] - ,[TrackingId] - ,[Type] - ,[SortOrder] - ,[Data] - ,[DateModifiedOffline] - ,[ServiceRequestId] - ,[Description] - ,[Comment] - ,[InternalComment] - ,[ServiceObjectId] - ,[StepListTemplateId] - ,[ParentId] - ,[DateDeleted] = @now - FROM @ARCHIVE - WHERE [ActionType] IN ( 'DELETE', 'UPDATE' ); + --INSERT INTO [dbo].[mfr__d_steps] + -- SELECT [Id] + -- ,[MobileId] + -- ,[Version] + -- ,[Name] + -- ,[IsDone] + -- ,[HasError] + -- ,[TrackingId] + -- ,[Type] + -- ,[SortOrder] + -- ,[Data] + -- ,[DateModifiedOffline] + -- ,[ServiceRequestId] + -- ,[Description] + -- ,[Comment] + -- ,[InternalComment] + -- ,[ServiceObjectId] + -- ,[StepListTemplateId] + -- ,[ParentId] + -- ,[DateDeleted] = @now + -- FROM @ARCHIVE + -- WHERE [ActionType] IN ( 'DELETE', 'UPDATE' ); END \ No newline at end of file diff --git a/Fuchs_Database/dbo/User Defined Types/fds__tt__mfr_items+.sql b/Fuchs_Database/dbo/User Defined Types/fds__tt__mfr_items+.sql new file mode 100644 index 0000000..47547c2 --- /dev/null +++ b/Fuchs_Database/dbo/User Defined Types/fds__tt__mfr_items+.sql @@ -0,0 +1,29 @@ +CREATE TYPE [dbo].[fds__tt__mfr_items+] AS TABLE ( + [Id] BIGINT NULL, + [Version] INT NULL, + [QuantityHours] NUMERIC (10, 3) NULL, + [PlannedQuantityHours] NUMERIC (10, 3) NULL, + [ItemTypeId] BIGINT NULL, + [ItemNumber] NVARCHAR (255) NULL, + [TrackingId] NVARCHAR (255) NULL, + [Manufacture] NVARCHAR (255) NULL, + [Price] NUMERIC (10, 3) NULL, + [Costs] NUMERIC (10, 3) NULL, + [NameOrNumber] NVARCHAR (255) NULL, + [CustomValues] NVARCHAR (MAX) NULL, + [Note] NVARCHAR (MAX) NULL, + [ExternalId] NVARCHAR (255) NULL, + [Discount] NUMERIC (10, 3) NULL, + [VAT] NUMERIC (10, 3) NULL, + [IsManual] BIT NULL, + [SortOrder] INT NULL, + [Type] NVARCHAR (255) NULL, + [ServiceRequestId] BIGINT NULL, + [ServiceObjectId] BIGINT NULL, + [CreatorId] BIGINT NULL, + [UnitId] BIGINT NULL, + [UnitString] NVARCHAR (255) NULL, + [baseorder] INT NULL, + [order] INT NULL, + [SetItmId] BIGINT NULL); + diff --git a/Fuchs_Database/dbo/User Defined Types/fds__tt__reminder_core.sql b/Fuchs_Database/dbo/User Defined Types/fds__tt__reminder_core.sql index f045a48..5c9c660 100644 --- a/Fuchs_Database/dbo/User Defined Types/fds__tt__reminder_core.sql +++ b/Fuchs_Database/dbo/User Defined Types/fds__tt__reminder_core.sql @@ -1,27 +1,28 @@ CREATE TYPE [dbo].[fds__tt__reminder_core] AS TABLE ( - [Id] VARCHAR (10) NULL, - [Version] INT NULL, - [DocumentName] VARCHAR (100) NULL, - [InvId] VARCHAR (15) NULL, - [CustomerId] BIGINT NULL, - [SendToAddress] NVARCHAR (1000) NULL, - [SendToEmail] NVARCHAR (255) NULL, - [type] VARCHAR (3) NULL, - [amount] NUMERIC (10, 3) NULL, - [amount_payed] NUMERIC (10, 3) NULL, - [amount_open] NUMERIC (10, 3) NULL, - [subject] NVARCHAR (255) NULL, - [text] NVARCHAR (2000) NULL, - [IsSent] BIT NULL, - [IsFinal] BIT NULL, - [CustomValues] NVARCHAR (MAX) NULL, - [DateSent] DATETIME NULL, - [UserSent] VARCHAR (25) NULL, - [DateFinalized] DATETIME NULL, - [UserFinalized] VARCHAR (25) NULL, - [DateCreated] DATETIME NULL, - [UserCreated] VARCHAR (25) NULL, - [DateModified] DATETIME NULL, - [UserModified] VARCHAR (25) NULL, - [file] VARBINARY (MAX) NULL); + [Id] VARCHAR (10) NULL, + [Version] INT NULL, + [DocumentName] VARCHAR (100) NULL, + [InvId] VARCHAR (15) NULL, + [CustomerId] BIGINT NULL, + [SendToAddress] NVARCHAR (1000) NULL, + [SendToEmail] NVARCHAR (255) NULL, + [type] VARCHAR (3) NULL, + [amount] NUMERIC (10, 3) NULL, + [amount_payed] NUMERIC (10, 3) NULL, + [amount_open] NUMERIC (10, 3) NULL, + [subject] NVARCHAR (255) NULL, + [text] NVARCHAR (2000) NULL, + [IsSent] BIT NULL, + [IsFinal] BIT NULL, + [CustomValues] NVARCHAR (MAX) NULL, + [DateSent] DATETIME NULL, + [UserSent] VARCHAR (25) NULL, + [DateFinalized] DATETIME NULL, + [UserFinalized] VARCHAR (25) NULL, + [DateCreated] DATETIME NULL, + [UserCreated] VARCHAR (25) NULL, + [DateModified] DATETIME NULL, + [UserModified] VARCHAR (25) NULL, + [file] VARBINARY (MAX) NULL, + [file_guid] UNIQUEIDENTIFIER NULL); diff --git a/MFR_RESTClient/MFR_RESTClient.csproj b/MFR_RESTClient/MFR_RESTClient.csproj index 6d45cd3..797d81c 100644 --- a/MFR_RESTClient/MFR_RESTClient.csproj +++ b/MFR_RESTClient/MFR_RESTClient.csproj @@ -26,21 +26,21 @@ - - + + - + - + - - - + + + - + - - + + diff --git a/OCORE b/OCORE index 973b474..91eb660 160000 --- a/OCORE +++ b/OCORE @@ -1 +1 @@ -Subproject commit 973b47439a7fb9d5ed2f38a44ae2c92786c514f6 +Subproject commit 91eb660610a0d05b2dad48c1e9051013e52d678a diff --git a/OCORE_Charting b/OCORE_Charting index 696d93a..69e7157 160000 --- a/OCORE_Charting +++ b/OCORE_Charting @@ -1 +1 @@ -Subproject commit 696d93abef14b35bcd3016bd5b4a40e2451b0892 +Subproject commit 69e71570a49d06249f4185aa75edcb5bb7d1f90e diff --git a/OCORE_web b/OCORE_web index 31a6151..552ceca 160000 --- a/OCORE_web +++ b/OCORE_web @@ -1 +1 @@ -Subproject commit 31a6151ee23f38ba2ee9db01c8990203fb5919ec +Subproject commit 552ceca7bf58ac5b120eeff253288cfd54296b0a diff --git a/eRechnungLib b/eRechnungLib index 3d37d8b..3f0bd93 160000 --- a/eRechnungLib +++ b/eRechnungLib @@ -1 +1 @@ -Subproject commit 3d37d8b08242c29900178a469823715cb14677b0 +Subproject commit 3f0bd93a62b7c9038971c56122ce246cc4eada8f