diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6dc5e8d..1598105 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -31,6 +31,7 @@ - Connection strings are stored under the standard `"ConnectionStrings"` key and read via `IConfiguration.GetConnectionString(...)`. - `FuchsOcmsIntranet.Initialize(configuration)` must be called at app start (in `Program.cs`) before DI registration; `Fuchs_intranet` receives `IConfiguration` via its constructor. - `appsettings.Development.json` (git-ignored) can override secrets for local development. +- `Fuchs:Email:OverrideRecipient` (bound via `IOptions`, section `Fuchs:Email`) is a dev/test safety net: when non-empty, `ProcessWebComService.SendEmailAsync` discards the real recipient of every outbound email and redirects it to this single address instead, so a locally-enabled mailer can never reach a real tenant-owner or end-customer. Set this only in `appsettings.Development.json` — it must stay empty in Production (`appsettings.json` documents the safe empty default). ## Libraries - Do not upgrade Spire.PDF beyond version 8.10.5. @@ -41,7 +42,7 @@ ## 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), `IPdfService` (MigraDoc render), `IInvoiceService`, `IReminderService`, `IReportService` (SQL report engine via `FuchsVisualization`), `IWidgetService`, `IBankingService`, `IMfrClientFactory`. + - `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`. - `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. @@ -65,7 +66,13 @@ - Tracing/metrics are always collected; OTLP export is opt-in via `Fuchs:Telemetry:OtlpEndpoint`. Don't add exporters that fail hard when no collector is present. ## Testing -- xUnit in `Fuchs.Tests`. For every service/handler change add tests covering **both** an intentionally succeeding and an intentionally failing path where feasible (use stubs/mocks; the test project has `InternalsVisibleTo`). DB-bound paths that can't be unit-tested should at least have their pure logic covered. +- xUnit in `Fuchs.Tests`. Testing must be **extensive**, not superficial: + - For every service/handler change add tests covering **both** an intentionally succeeding and an intentionally failing path where feasible (use stubs/mocks; the test project has `InternalsVisibleTo`). + - Cover edge cases and boundary conditions (empty/null/invalid input, disabled/feature-flag-off states, API/network errors) via `[Theory]`/`[InlineData]`/`[MemberData]` rather than a single happy-path `[Fact]`. + - When behavior depends on configuration (e.g. `appsettings.json` vs `appsettings.Development.json`), prefer loading the **real** files layered the same way ASP.NET Core does (`ConfigurationBuilder` + `AddJsonFile`) over hand-typed literals only, so drift in the actual files is caught (see `ProcessWebComServiceTests.SendEmailAsync_OverrideRecipientFromRealAppsettings_ClearsRecipientWheneverConfigured` for the pattern). + - Assert on the observable contract (e.g. the outgoing request payload) and on emitted telemetry (counters/histograms) when the code records them — not just the boolean return value. + - Name tests `MethodName_Scenario_ExpectedResult`. + - DB-bound paths that can't be unit-tested should at least have their pure logic covered. ## Azure Key Vault — Secret Naming - Secret names must satisfy the pattern `^[0-9a-zA-Z-]+$` (alphanumerics and hyphens only; no underscores, dots, or spaces). diff --git a/CLAUDE.md b/CLAUDE.md index 618ceef..aa9f714 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# CLAUDE.md — Project instructions for Claude Code +# CLAUDE.md — Project instructions for Claude Code > ## ⚠️ Instruction Sync > This file and **`.github/copilot-instructions.md`** are two views of the same @@ -29,13 +29,14 @@ - All settings in `Fuchs/appsettings.json` — **never** `Web.config` / `System.Configuration.ConfigurationManager`. App settings nested under `"Fuchs"`; connection strings under `"ConnectionStrings"`. - `FuchsOcmsIntranet.Initialize(configuration)` runs in `Program.cs` before DI registration. - `appsettings.Development.json` (git-ignored) overrides secrets locally. +- `Fuchs:Email:OverrideRecipient` (`IOptions`, section `Fuchs:Email`) is a dev/test safety net: when non-empty, `ProcessWebComService.SendEmailAsync` discards the real recipient of every outbound email and redirects it to this single address instead, so a locally-enabled mailer can never reach a real tenant-owner or end-customer. Set only in `appsettings.Development.json` — must stay empty in Production. ## Libraries - Do **not** upgrade Spire.PDF beyond 8.10.5. Prefer OCORE / OCORE_web / OCORE_web_pdf helpers over rewriting. Do not use OCMS/OCMS_sharp — OCORE only. ## 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`, `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`. 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). @@ -57,7 +58,13 @@ - Always collected; OTLP export opt-in via `Fuchs:Telemetry:OtlpEndpoint`. No exporters that hard-fail without a collector. ## Testing -- xUnit in `Fuchs.Tests`. For each service/handler change, add tests for **both** an intentionally succeeding and an intentionally failing path where feasible (stubs/mocks; `InternalsVisibleTo` is enabled). Cover pure logic for DB-bound paths that can't be unit-tested. +- xUnit in `Fuchs.Tests`. Testing must be **extensive**, not superficial: + - For each service/handler change, cover **both** an intentionally succeeding and an intentionally failing path where feasible (stubs/mocks; `InternalsVisibleTo` is enabled). + - Cover edge cases and boundary conditions (empty/null/invalid input, disabled/feature-flag-off states, API/network errors) via `[Theory]`/`[InlineData]`/`[MemberData]` rather than a single happy-path `[Fact]`. + - When behavior depends on configuration (e.g. `appsettings.json` vs `appsettings.Development.json`), prefer loading the **real** files layered the same way ASP.NET Core does (`ConfigurationBuilder` + `AddJsonFile`) over hand-typed literals only, so drift in the actual files is caught (see `ProcessWebComServiceTests.SendEmailAsync_OverrideRecipientFromRealAppsettings_ClearsRecipientWheneverConfigured` for the pattern). + - Assert on the observable contract (e.g. the outgoing request payload) and on emitted telemetry (counters/histograms) when the code records them — not just the boolean return value. + - Name tests `MethodName_Scenario_ExpectedResult`. + - Cover pure logic for DB-bound paths that can't be unit-tested. ## Secrets (Azure Key Vault) - Full naming rules live in `.github/copilot-instructions.md` (kept in sync). In short: names match `^[0-9a-zA-Z-]+$`, hierarchy via `--` (→ `:`), underscores → `-`, app prefix `fuchs`; register new keys in `ManagedSecretKeys` in `appsettings.json`. diff --git a/Fuchs.Tests/AzureBlobStorageServiceTests.cs b/Fuchs.Tests/AzureBlobStorageServiceTests.cs new file mode 100644 index 0000000..574ff25 --- /dev/null +++ b/Fuchs.Tests/AzureBlobStorageServiceTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Fuchs.Services; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Tests for the Azure Blob Storage secondary-archive service. Covers the +/// disabled/unconfigured path (no client), the empty-content guard, successful +/// uploads (routing to the correct per-category container and blob name), an +/// upload that throws (must be swallowed, never break invoice/reminder storage), +/// and the emitted telemetry counters. +/// +/// Azure.Storage.Blobs clients (BlobServiceClient/BlobContainerClient/BlobClient) +/// expose a protected parameterless constructor and virtual members specifically +/// to support mocking with Moq — see Azure SDK unit-testing guidance. +/// +public class AzureBlobStorageServiceTests +{ + private const string FuchsMeterName = "Fuchs.Intranet"; + + private static AzureBlobStorageSettings CreateSettings() => new() + { + Enabled = true, + InvoiceContainer = "test-invoices", + ReminderContainer = "test-reminders" + }; + + private static (Mock service, Mock container, Mock blob) + CreateMockedClientChain() + { + var blobClientMock = new Mock(); + var containerClientMock = new Mock(); + containerClientMock.Setup(c => c.GetBlobClient(It.IsAny())).Returns(blobClientMock.Object); + containerClientMock + .Setup(c => c.CreateIfNotExistsAsync( + It.IsAny(), It.IsAny>(), + It.IsAny(), It.IsAny())) + .ReturnsAsync((Response)null!); + + var serviceClientMock = new Mock(); + serviceClientMock.Setup(s => s.GetBlobContainerClient(It.IsAny())).Returns(containerClientMock.Object); + + return (serviceClientMock, containerClientMock, blobClientMock); + } + + // ── Disabled / unconfigured (feature-flag-off) ───────────────────────────── + [Fact] + public async Task UploadInvoicePdfAsync_NoClient_ReturnsNullWithoutThrowing() + { + var svc = new AzureBlobStorageService(client: null, CreateSettings(), NullLogger.Instance); + + Uri? result = await svc.UploadInvoicePdfAsync("INV1", "Rechnung_INV1.pdf", new byte[] { 1, 2, 3 }); + + Assert.Null(result); + } + + // ── Boundary condition: empty content ────────────────────────────────────── + [Fact] + public async Task UploadReminderPdfAsync_EmptyContent_ReturnsNullWithoutCallingClient() + { + var (service, container, blob) = CreateMockedClientChain(); + var svc = new AzureBlobStorageService(service.Object, CreateSettings(), NullLogger.Instance); + + Uri? result = await svc.UploadReminderPdfAsync("REM1", "Zahlungserinnerung_REM1.pdf", Array.Empty()); + + Assert.Null(result); + container.Verify(c => c.CreateIfNotExistsAsync( + It.IsAny(), It.IsAny>(), + It.IsAny(), It.IsAny()), Times.Never); + blob.Verify(b => b.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + // ── Success path: invoice routing + counter ───────────────────────────────── + [Fact] + public async Task UploadInvoicePdfAsync_ClientSucceeds_ReturnsBlobUriAndUsesInvoiceContainer() + { + var expectedUri = new Uri("https://test.blob.core.windows.net/test-invoices/INV1/Rechnung_INV1.pdf"); + var (service, container, blob) = CreateMockedClientChain(); + blob.Setup(b => b.Uri).Returns(expectedUri); + blob.Setup(b => b.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Response)null!); + + long delta = 0; + using var listener = new MeterListener + { + InstrumentPublished = (inst, l) => + { + if (inst.Meter.Name == FuchsMeterName && inst.Name == "fuchs.blobstorage.uploads") + l.EnableMeasurementEvents(inst); + } + }; + listener.SetMeasurementEventCallback((_, value, _, _) => Interlocked.Add(ref delta, value)); + listener.Start(); + + var svc = new AzureBlobStorageService(service.Object, CreateSettings(), NullLogger.Instance); + Uri? result = await svc.UploadInvoicePdfAsync("INV1", "Rechnung_INV1.pdf", new byte[] { 1, 2, 3, 4 }); + + Assert.Equal(expectedUri, result); + service.Verify(s => s.GetBlobContainerClient("test-invoices"), Times.Once); + container.Verify(c => c.GetBlobClient("INV1/Rechnung_INV1.pdf"), Times.Once); + blob.Verify(b => b.UploadAsync(It.IsAny(), true, It.IsAny()), Times.Once); + Assert.True(delta >= 1, "fuchs.blobstorage.uploads counter should have been incremented on a successful upload."); + } + + // ── Success path: reminder routing (different container) ────────────────── + [Fact] + public async Task UploadReminderPdfAsync_ClientSucceeds_UsesReminderContainer() + { + var (service, container, blob) = CreateMockedClientChain(); + blob.Setup(b => b.Uri).Returns(new Uri("https://test.blob.core.windows.net/test-reminders/REM2/Zahlungserinnerung_REM2.pdf")); + blob.Setup(b => b.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Response)null!); + + var svc = new AzureBlobStorageService(service.Object, CreateSettings(), NullLogger.Instance); + Uri? result = await svc.UploadReminderPdfAsync("REM2", "Zahlungserinnerung_REM2.pdf", new byte[] { 5, 6 }); + + Assert.NotNull(result); + service.Verify(s => s.GetBlobContainerClient("test-reminders"), Times.Once); + container.Verify(c => c.GetBlobClient("REM2/Zahlungserinnerung_REM2.pdf"), Times.Once); + } + + // ── Boundary condition: blank filename falls back to "{id}.pdf" ─────────── + [Fact] + public async Task UploadInvoicePdfAsync_BlankFileName_FallsBackToIdPdfBlobName() + { + var (service, container, blob) = CreateMockedClientChain(); + blob.Setup(b => b.Uri).Returns(new Uri("https://test.blob.core.windows.net/test-invoices/INV3/INV3.pdf")); + blob.Setup(b => b.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((Response)null!); + + var svc = new AzureBlobStorageService(service.Object, CreateSettings(), NullLogger.Instance); + Uri? result = await svc.UploadInvoicePdfAsync("INV3", " ", new byte[] { 7 }); + + Assert.NotNull(result); + container.Verify(c => c.GetBlobClient("INV3/INV3.pdf"), Times.Once); + } + + // ── Failure path: client throws, must not propagate ──────────────────────── + [Fact] + public async Task UploadReminderPdfAsync_ClientThrows_ReturnsNullAndIncrementsFailedCounter() + { + var (service, container, blob) = CreateMockedClientChain(); + blob.Setup(b => b.UploadAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new RequestFailedException("simulated storage failure")); + + long delta = 0; + using var listener = new MeterListener + { + InstrumentPublished = (inst, l) => + { + if (inst.Meter.Name == FuchsMeterName && inst.Name == "fuchs.blobstorage.uploads.failed") + l.EnableMeasurementEvents(inst); + } + }; + listener.SetMeasurementEventCallback((_, value, _, _) => Interlocked.Add(ref delta, value)); + listener.Start(); + + var svc = new AzureBlobStorageService(service.Object, CreateSettings(), NullLogger.Instance); + Uri? result = await svc.UploadReminderPdfAsync("REM3", "Zahlungserinnerung_REM3.pdf", new byte[] { 9, 9 }); + + Assert.Null(result); + Assert.True(delta >= 1, "fuchs.blobstorage.uploads.failed counter should have been incremented on a failed upload."); + } +} diff --git a/Fuchs.Tests/DocumentArchiveSyncServiceTests.cs b/Fuchs.Tests/DocumentArchiveSyncServiceTests.cs new file mode 100644 index 0000000..6080e2a --- /dev/null +++ b/Fuchs.Tests/DocumentArchiveSyncServiceTests.cs @@ -0,0 +1,59 @@ +using System.Threading; +using System.Threading.Tasks; +using Fuchs.intranet; +using Fuchs.Services; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Tests for — the one-shot startup backfill for the +/// Azure Blob Storage secondary archive. Only the disabled/feature-flag-off fast path is +/// unit-testable without a real SQL Server connection (the enabled path drives +/// fds__getInvoiceFiles_ForBlobArchive / fds__getReminderFiles_ForBlobArchive against +/// fds__invoices / fds__reminder); the enabled path is exercised manually/in integration +/// environments, consistent with InvoiceService/ReminderService which are likewise DB-bound +/// and have no unit tests of their SQL-calling members. +/// +public class DocumentArchiveSyncServiceTests +{ + private static Fuchs_intranet CreateIntranet() => + new(new ConfigurationBuilder().Build()); + + [Fact] + public async Task ExecuteAsync_FeatureDisabled_NeverTouchesBlobStorageAndCompletesImmediately() + { + var blobStorage = new Mock(MockBehavior.Strict); + var settings = Options.Create(new AzureBlobStorageSettings { Enabled = false }); + using var service = new DocumentArchiveSyncService( + CreateIntranet(), blobStorage.Object, settings, NullLogger.Instance); + + await service.StartAsync(CancellationToken.None); + await service.StopAsync(CancellationToken.None); + + blobStorage.VerifyNoOtherCalls(); + } + + [Fact] + public async Task ExecuteAsync_FeatureDisabled_DoesNotThrowEvenWithUnconfiguredIntranet() + { + // The disabled fast path must return before any SQL access is attempted, so an + // intranet instance with no real connection string configured is still safe to use. + var blobStorage = new Mock(MockBehavior.Strict); + var settings = Options.Create(new AzureBlobStorageSettings { Enabled = false }); + using var service = new DocumentArchiveSyncService( + CreateIntranet(), blobStorage.Object, settings, NullLogger.Instance); + + var exception = await Record.ExceptionAsync(async () => + { + await service.StartAsync(CancellationToken.None); + await service.StopAsync(CancellationToken.None); + }); + + Assert.Null(exception); + } +} diff --git a/Fuchs.Tests/DocumentMetadataBuilderTests.cs b/Fuchs.Tests/DocumentMetadataBuilderTests.cs new file mode 100644 index 0000000..7cd869b --- /dev/null +++ b/Fuchs.Tests/DocumentMetadataBuilderTests.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using Fuchs.Services; +using Xunit; + +namespace Fuchs.Tests; + +/// +/// Tests for — the per-blob metadata projection used by the +/// Azure Blob Storage archive (see and +/// ). Covers the "skip if absent from the row, keep empty +/// string if present-but-empty" contract, case-insensitive column lookup (SQL rows are frequently +/// lower-cased), and value stringification (DateTime round-trip formatting, DBNull/null handling). +/// +public class DocumentMetadataBuilderTests +{ + // ── Field presence contract ────────────────────────────────────────────── + [Fact] + public void Build_FieldPresentWithValue_IncludesStringifiedValue() + { + var row = new Dictionary { ["InvoiceId"] = "INV-42" }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "InvoiceId" }); + + Assert.Equal("INV-42", metadata["InvoiceId"]); + } + + [Fact] + public void Build_FieldAbsentFromRow_SkipsFieldEntirely() + { + // Reminders have no file_guid column — must be skipped, not stored as empty. + var row = new Dictionary { ["Id"] = "REM1" }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "Id", "file_guid" }); + + Assert.True(metadata.ContainsKey("Id")); + Assert.False(metadata.ContainsKey("file_guid")); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Build_FieldPresentButNullOrEmpty_StoresEmptyStringRatherThanSkipping(object? value) + { + var row = new Dictionary { ["InvoiceTitle"] = value }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "InvoiceTitle" }); + + Assert.True(metadata.ContainsKey("InvoiceTitle")); + Assert.Equal("", metadata["InvoiceTitle"]); + } + + [Fact] + public void Build_FieldPresentButDBNull_StoresEmptyString() + { + var row = new Dictionary { ["InvoiceTitle"] = DBNull.Value }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "InvoiceTitle" }); + + Assert.True(metadata.ContainsKey("InvoiceTitle")); + Assert.Equal("", metadata["InvoiceTitle"]); + } + + // ── Case-insensitive lookup ────────────────────────────────────────────── + [Theory] + [InlineData("invoiceid")] + [InlineData("INVOICEID")] + [InlineData("InVoIcEId")] + public void Build_CaseInsensitiveRowKey_MatchesConfiguredFieldName(string rowKey) + { + var row = new Dictionary { [rowKey] = "INV-7" }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "InvoiceId" }); + + Assert.Equal("INV-7", metadata["InvoiceId"]); + } + + // ── Value stringification ──────────────────────────────────────────────── + [Fact] + public void Build_DateTimeValue_FormatsAsRoundTripString() + { + var dt = new DateTime(2026, 3, 14, 9, 30, 0, DateTimeKind.Utc); + var row = new Dictionary { ["DateCreated"] = dt }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "DateCreated" }); + + Assert.Equal(dt.ToString("O"), metadata["DateCreated"]); + } + + [Theory] + [InlineData(42, "42")] + [InlineData(true, "True")] + public void Build_NonStringValue_UsesToString(object value, string expected) + { + var row = new Dictionary { ["Version"] = value }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "Version" }); + + Assert.Equal(expected, metadata["Version"]); + } + + [Fact] + public void Build_GuidValue_UsesDefaultGuidFormat() + { + var guid = Guid.Parse("11111111-1111-1111-1111-111111111111"); + var row = new Dictionary { ["file_guid"] = guid }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "file_guid" }); + + Assert.Equal(guid.ToString(), metadata["file_guid"]); + } + + // ── Field-list edge cases ──────────────────────────────────────────────── + [Theory] + [InlineData("")] + [InlineData(" ")] + public void Build_WhitespaceFieldName_IsSkipped(string field) + { + var row = new Dictionary { ["Id"] = "X1" }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { field, "Id" }); + + Assert.Single(metadata); + Assert.Equal("X1", metadata["Id"]); + } + + [Fact] + public void Build_EmptyFieldsList_ReturnsEmptyDictionary() + { + var row = new Dictionary { ["Id"] = "X1" }; + + var metadata = DocumentMetadataBuilder.Build(row, Array.Empty()); + + Assert.Empty(metadata); + } + + [Fact] + public void Build_EmptyRow_SkipsAllConfiguredFields() + { + var row = new Dictionary(); + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "Id", "Version", "InvoiceId" }); + + Assert.Empty(metadata); + } + + [Fact] + public void Build_DuplicateFieldNamesInFieldList_ProducesSingleEntry() + { + var row = new Dictionary { ["Id"] = "X1" }; + + var metadata = DocumentMetadataBuilder.Build(row, new[] { "Id", "Id" }); + + Assert.Single(metadata); + Assert.Equal("X1", metadata["Id"]); + } + + // ── Realistic per-document-type row shapes ─────────────────────────────── + [Fact] + public void Build_InvoiceRowShape_ProjectsAllConfiguredInvoiceFieldsAndSkipsReminderOnlyColumn() + { + var row = new Dictionary + { + ["Id"] = "INV1", + ["Version"] = 2, + ["InvoiceId"] = "R-2026-001", + ["InvoiceTitle"] = "Rechnung", + ["DocumentName"] = "Rechnung_INV1.pdf", + ["file_guid"] = Guid.Parse("11111111-1111-1111-1111-111111111111") + }; + var fields = new[] { "Id", "Version", "InvoiceId", "InvoiceTitle", "InvId", "DocumentName", "file_guid" }; + + var metadata = DocumentMetadataBuilder.Build(row, fields); + + Assert.Equal("INV1", metadata["Id"]); + Assert.Equal("2", metadata["Version"]); + Assert.Equal("R-2026-001", metadata["InvoiceId"]); + Assert.Equal("Rechnung", metadata["InvoiceTitle"]); + Assert.Equal("Rechnung_INV1.pdf", metadata["DocumentName"]); + Assert.Equal("11111111-1111-1111-1111-111111111111", metadata["file_guid"]); + Assert.False(metadata.ContainsKey("InvId"), "Invoices have no InvId column — must be skipped, not empty."); + } + + [Fact] + public void Build_ReminderRowShape_SkipsInvoiceOnlyColumns() + { + var row = new Dictionary + { + ["Id"] = "REM1", + ["Version"] = 0, + ["InvId"] = "INV1", + ["DocumentName"] = "Zahlungserinnerung_REM1.pdf" + }; + var fields = new[] { "Id", "Version", "InvoiceId", "InvoiceTitle", "InvId", "DocumentName", "file_guid" }; + + var metadata = DocumentMetadataBuilder.Build(row, fields); + + Assert.Equal("REM1", metadata["Id"]); + Assert.Equal("0", metadata["Version"]); + Assert.Equal("INV1", metadata["InvId"]); + Assert.Equal("Zahlungserinnerung_REM1.pdf", metadata["DocumentName"]); + Assert.False(metadata.ContainsKey("InvoiceId")); + Assert.False(metadata.ContainsKey("InvoiceTitle")); + Assert.False(metadata.ContainsKey("file_guid")); + } +} diff --git a/Fuchs.Tests/Fuchs.Tests.csproj b/Fuchs.Tests/Fuchs.Tests.csproj index 9121014..3cfb7ed 100644 --- a/Fuchs.Tests/Fuchs.Tests.csproj +++ b/Fuchs.Tests/Fuchs.Tests.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Fuchs.Tests/ProcessWebComServiceTests.cs b/Fuchs.Tests/ProcessWebComServiceTests.cs index ff14a0b..ce94512 100644 --- a/Fuchs.Tests/ProcessWebComServiceTests.cs +++ b/Fuchs.Tests/ProcessWebComServiceTests.cs @@ -1,11 +1,13 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics.Metrics; +using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Fuchs.Services; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; @@ -53,7 +55,7 @@ public class ProcessWebComServiceTests public HttpClient CreateClient(string name) => new(_handler, disposeHandler: false); } - private static ProcessWebComService CreateService(StubHandler handler, bool enabled = true) + private static ProcessWebComService CreateService(StubHandler handler, bool enabled = true, string? overrideRecipient = null) { var settings = Options.Create(new ProcessWebComSettings { @@ -62,10 +64,15 @@ public class ProcessWebComServiceTests AccountId = "acct", Token = "tok" }); + var emailSettings = Options.Create(new FuchsEmailSettings + { + OverrideRecipient = overrideRecipient + }); return new ProcessWebComService( NullLogger.Instance, intranet: null!, settings, + emailSettings, new StubHttpClientFactory(handler)); } @@ -122,6 +129,129 @@ public class ProcessWebComServiceTests Assert.Equal(0, handler.CallCount); } + // ── Dev/test recipient override safety net ───────────────────────────────── + [Fact] + public async Task SendEmailAsync_OverrideRecipientSet_RedirectsToOverrideAddress() + { + var handler = new StubHandler(HttpStatusCode.OK); + var svc = CreateService(handler, overrideRecipient: "dev-inbox@example.test"); + + bool result = await svc.SendEmailAsync("inv_ov1", "Subject", "

hi

", "realcustomer@example.de", "Kunde"); + + Assert.True(result); + Assert.Equal(1, handler.CallCount); + var json = JObject.Parse(handler.LastRequestBody!); + Assert.Equal("dev-inbox@example.test", json["recipient"]!.ToString()); + } + + [Fact] + public async Task SendEmailAsync_OverrideRecipientSet_SubjectRetainsOriginalRecipientForTraceability() + { + var handler = new StubHandler(HttpStatusCode.OK); + var svc = CreateService(handler, overrideRecipient: "dev-inbox@example.test"); + + await svc.SendEmailAsync("inv_ov2", "Rechnung 123", "

hi

", "realcustomer@example.de", "Kunde"); + + var json = JObject.Parse(handler.LastRequestBody!); + string subject = json["subject"]!.ToString(); + Assert.Contains("realcustomer@example.de", subject); + Assert.Contains("Rechnung 123", subject); + } + + [Fact] + public async Task SendEmailAsync_OverrideRecipientSet_InvalidOriginalAddressIsStillRedirected() + { + var handler = new StubHandler(HttpStatusCode.OK); + var svc = CreateService(handler, overrideRecipient: "dev-inbox@example.test"); + + bool result = await svc.SendEmailAsync("inv_ov3", "Subject", "

hi

", "not-an-email", "Kunde"); + + Assert.True(result); + Assert.Equal(1, handler.CallCount); + var json = JObject.Parse(handler.LastRequestBody!); + Assert.Equal("dev-inbox@example.test", json["recipient"]!.ToString()); + } + + [Fact] + public async Task SendEmailAsync_NoOverrideConfigured_SendsToOriginalRecipient() + { + var handler = new StubHandler(HttpStatusCode.OK); + var svc = CreateService(handler); + + 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()); + } + + // ── Override enforcement across every real appsettings*.json environment ─── + // Loads the ACTUAL Fuchs/appsettings*.json files the same way ASP.NET Core + // layers them (base file + optional environment-specific overlay). These files + // are copied into this test assembly's output directory via the Fuchs project + // reference (Microsoft.NET.Sdk.Web auto-includes appsettings*.json as Content + // with CopyToOutputDirectory). This proves the safety net holds for whichever + // environment/appsettings file ends up "active" at runtime - not just a + // 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 + // to/cc/bcc concept anywhere in the codebase (see ProcessWebComService. + // SendEmailAsync / payload.recipient). "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() + { + yield return new object[] { "" }; // base appsettings.json only ("Production"-like default) + foreach (var file in Directory.EnumerateFiles(AppContext.BaseDirectory, "appsettings.*.json")) + { + string[] parts = Path.GetFileName(file).Split('.'); + if (parts.Length == 3 && parts[0] == "appsettings" && parts[2] == "json") + yield return new object[] { parts[1] }; + } + } + + private static string ResolveOverrideRecipientForEnvironment(string environmentName) + { + var builder = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); + if (!string.IsNullOrEmpty(environmentName)) + builder.AddJsonFile($"appsettings.{environmentName}.json", optional: true, reloadOnChange: false); + return builder.Build()["Fuchs:Email:OverrideRecipient"] ?? ""; + } + + [Theory] + [MemberData(nameof(AppSettingsEnvironments))] + public async Task SendEmailAsync_OverrideRecipientFromRealAppsettings_ClearsRecipientWheneverConfigured(string environmentName) + { + string overrideRecipient = ResolveOverrideRecipientForEnvironment(environmentName); + var handler = new StubHandler(HttpStatusCode.OK); + var svc = CreateService(handler, overrideRecipient: overrideRecipient); + + const string originalRecipient = "realcustomer@tenant-owner.example"; + bool result = await svc.SendEmailAsync( + "env_" + (string.IsNullOrEmpty(environmentName) ? "base" : environmentName), + "Subject", "

hi

", originalRecipient, "Kunde"); + + Assert.True(result); + Assert.Equal(1, handler.CallCount); + var json = JObject.Parse(handler.LastRequestBody!); + string sentRecipient = json["recipient"]!.ToString(); + + if (!string.IsNullOrWhiteSpace(overrideRecipient)) + { + // Override configured for this environment: the real recipient (to/cc/bcc) + // must be fully cleared and replaced by the override - never leaked. + Assert.Equal(overrideRecipient, sentRecipient); + Assert.NotEqual(originalRecipient, sentRecipient); + } + else + { + // No override configured for this environment: real recipient is used normally. + Assert.Equal(originalRecipient, sentRecipient); + } + } + // ── Attachment payload contract ──────────────────────────────────────────── [Fact] public async Task SendEmailAsync_WithAttachment_EmbedsBase64InPayload() diff --git a/Fuchs/Docs/INVOICE_LIFECYCLE.md b/Fuchs/Docs/INVOICE_LIFECYCLE.md new file mode 100644 index 0000000..90b3cb6 --- /dev/null +++ b/Fuchs/Docs/INVOICE_LIFECYCLE.md @@ -0,0 +1,359 @@ +# Invoice Lifecycle — From First Click to Customer Email + +Concept document describing the full, end-to-end invoicing process in the +Fuchs Intranet: how an office user turns a completed service request into an +invoice, how the invoice is drafted/previewed/edited, how it is finalised, and +how it reaches the customer by email (including resend and reminders). This +complements [`INVOICE_SET_PRICING.md`](INVOICE_SET_PRICING.md) (pricing/display +rules for set items) and [`EVAL_live_invoice_editing.md`](EVAL_live_invoice_editing.md) +(why the editor is stateless). + +> Scope note: this document describes the **implemented** flow in `Fuchs` +> (ASP.NET Core MVC intranet, jQuery front-end). It does not cover the Razor +> Pages areas of the workspace — the intranet/invoice module predates those and +> is intentionally kept as-is (see `.github/copilot-instructions.md`). + +## 1. Actors & building blocks + +| Layer | Files | Responsibility | +|---|---|---| +| **Browser UI** | `js/intranet/modules/fis.req.js`, `fis.inv_shared.js`, `fis.inv.js` (bundled to `wwwroot/web/fis.req.de.js` / `fis.inv.de.js` via gulp `min:js`) | Request list, invoice editor dialog, PDF preview rendering, all user interaction | +| **Controller** | `Controllers/IntranetController.Requests.cs`, `.Invoices.cs`, `.Invoices2.cs`, `.Reminder.cs` | Thin action dispatch (`req/*`, `inv/*`, `rem/*`), auth checks, request/response shaping | +| **Services (DI)** | `Services/IInvoiceService` + `InvoiceService`, `Services/IComService` + `ProcessWebComService`, `Services/IPdfService` + `FuchsPdfService`, `Services/IReminderService` + `ReminderService` | Register/render/store invoices, send email, render PDF documents | +| **Data model** | `code/FdsInvoiceData.cs` | Pure data holder: parses the posted `invc` JSON, builds SQL parameters (`BuildInvoiceParams`), exposes registration fields | +| **PDF rendering** | `code/FuchsPdf.cs` (`ApplyInvoice`, `CreatePage_Letter`, `AddGirocode`), `code/InvoiceSetPricing.cs` | MigraDoc layout, draft watermark, GiroCode SEPA QR, set-pricing line transformation | +| **Database** | `Fuchs_Database` SSDT project — `fds__prepInvoice`, `fds__createInvoice`, `fds__setInvoice`, `fds__createInvoice_Details`, `fds__setInvoiceFinal`, `fds__setInvoiceFile`, `fds__setInvoiceSent`, `fds__getInvoice`, `fds__newInvoiceId` | SQL-first persistence, invoice numbering, finalisation, sent/paid status | + +All backend I/O is SQL-first via OCORE helpers (`getSQLDataSet_async`, +`setSQLValue_async`) and stored procedures — there is no EF Core in this path +(see `.github/instructions/ocore.instructions.md`). + +## 2. High-level lifecycle + +```mermaid +flowchart LR + A[Service request\ncompleted] --> B[Create invoice\nfrom request] + B --> C[Draft invoice\nregistered] + C --> D[Preview / Edit\nloop] + D -->|adjust items,\naddress, set-mode...| D + D -->|confirm| E[Finalise\ninvoice] + E --> F[Render + store\nfinal PDF] + F --> G[Email to\ncustomer] + G --> H[Mark as sent] + H -.optional.-> I[Resend email] + H -.optional.-> J[Reminder /\nMahnung] + E -.optional.-> K[Storno / Credit\nnote] +``` + +## 3. Entry point — from the request list + +Invoices are **not** created from a standalone "new invoice" wizard; they are +always created *from* one or more completed service requests. + +1. The office user opens the **Aufträge** (requests) list, rendered by + `fis.req.js`. `$req.init2/init3` load the list from `req/reql` + (`HandleRequestList` → `fds__getRequests_list[2]`). +2. Selecting a request row and clicking the invoice icon calls + `$inv.cInv` (bound in `fis.req.js` via `.click({ id: rw.Id }, $inv.cInv)`), + which — after an auth check (`fds_inv` level 2) — calls `$inv.cInv2({ id })`. +3. `$inv.cInv2` posts to **`req/get`** (`HandleRequestGet` → + `fds__getRequest_details`, mode `r`) to load the request(s) plus any + already-linked invoice, then renders the invoice editor dialog + (`invoice_layout`) and wires `$inv.eM` (the contextual top menu: save, + set-mode switch, §13b toggle, contact-person edit). +4. Multiple requests can be bundled onto a single invoice (the editor supports + several request "blocks", each becoming its own item group). + +```mermaid +sequenceDiagram + participant U as User (browser) + participant JS as fis.req.js / fis.inv_shared.js + participant C as IntranetController + participant DB as SQL (fds__*) + + U->>JS: click invoice icon on request row + JS->>JS: $inv.cInv -> $inv.cInv2({id}) + JS->>C: POST req/get {id, mode:'r'} + C->>DB: EXEC fds__getRequest_details + DB-->>C: admin, requests, items, inv + C-->>JS: JSON {admin, requests, inv} + JS->>U: render invoice editor dialog (items, totals, address, email) +``` + +## 4. Drafting, preview and editing + +The invoice editor is **stateless**: the browser holds the working model +(`table.invi` jQuery `.data()`), the server never caches a partial invoice +between requests. Every preview/save round-trip posts the *entire* invoice +payload; see `EVAL_live_invoice_editing.md` for the rationale. + +### 4.1 What the user can change +- **Line items** — quantities, prices, notes, combine into one sum + (`$inv.rendersrq`, `$inv.quantChange`). +- **Recipient fields** — invoice title, address, email, provision + location/period (inline edit fields, `fm(...)` helper in `fis.inv_shared.js`). +- **§13b reverse-charge** toggle (`$inv.sp13b`) — suppresses VAT lines/columns. +- **Set-pricing display mode** (`$inv.ssetmode` / `setSetmode`) — `SetPrice` + (default) / `ItemPrices` / `SetOnly`; see `INVOICE_SET_PRICING.md`. Purely + presentational — totals never change. +- **Contact person** for the invoice (`$inv.sctp`, stored in `CustomValues`). + +All of this recalculates client-side totals live via the `fds.inv` event +(`$inv.invSumUpdate`), which also builds the **backend item contract** +(`$inv.itemToContract`: `{ id, type, title, desc, qty, price_net, total_net, +vat }` plus set header/member tagging) that is posted to the server. + +### 4.2 Posting a draft / preview + +| User action | JS entry point | Endpoint | Controller handler | Effect | +|---|---|---|---|---| +| First save without preview | `$inv.ssave` | `req/save` | `Do_Process_Requests` case `save` | `RegisterInvoiceAsync(change: id present)` — creates or updates the draft row, **no PDF rendered** | +| Preview a **new** invoice | `$inv.sprev(false)` | `req/sprep` | case `sprep` | Registers draft, then `GenerateInvoicePdf` → renders preview images | +| Preview an **existing** draft | `$inv.sprev(true)` (aka `$inv.sedit`) | `req/sedit` | case `sedit` | Same as `sprep` but requires `id`, updates existing draft | +| Cancel out of preview | dialog `cancel` handler | `req/sdel` | case `sdel` | `fds__remInvoice` — deletes the (unfinalised) draft row | + +Before posting, `$inv.invcPayload(d)` normalizes the editor's internal field +names into the exact names `FdsInvoiceData.BuildInvoiceParams` reads (totals +from `sms.ttn/ttb`, `invoicetitle→title`, `loc→provisionlocation`, +`paymentterms→paymentterm`, etc. — see `INVOICE_SET_PRICING.md` for the full +mapping table). The posted shape is always `{ admin, req, sms, new }`. + +On the server, `RegisterInvoiceAsync` (in `InvoiceService`) turns the posted +JSON into SQL parameters and calls, in one batch: +- **New invoice**: `fds__createInvoice` (allocates the `Id`, returns a fresh + row) → `fds__createInvoice_Details` (service net/VAT + `InvoiceOptions`, + e.g. `setmode:itemprices`, `§13b`). +- **Existing draft**: `fds__setInvoice` (same parameter set, updates in place) + → `fds__createInvoice_Details` again. + +`GenerateInvoicePdf` (still in `InvoiceService`) builds `FuchsPdf.FdsTextBlocks` +and calls `IPdfService.WriteLetterAsync` + `ApplyInvoice`, which internally +uses `InvoiceSetPricing.Build(...)` to turn the posted item contract into +ordered, priced/unpriced lines. While the invoice is **not yet final** +(`IsDraft == true`), the rendered PDF carries a diagonal **"Entwurf"/draft +overlay watermark** (`CreatePage_Letter(..., draft: true)` stamps +`Data/overlay.png`) and — importantly — **no GiroCode payment QR** is added +(`AddGirocode` is only called `if (!inv.IsDraft && payAmount > 0 ...)`). +The preview images are returned as base64 (`DocToImageCollectionAsync`) and +shown in a modal (`$ocms.dlg(... form:false, button:'Rechnung erstellen' ...)`). + +```mermaid +sequenceDiagram + participant U as User + participant JS as fis.inv_shared.js + participant C as IntranetController.Requests + participant S as InvoiceService + participant DB as SQL + + U->>JS: edit items / address / set-mode + JS->>JS: fds.inv event -> invSumUpdate (recalculate + build items[]) + U->>JS: click "Vorschau" (preview) + JS->>C: POST req/sprep or req/sedit {invc: JSON, id?} + C->>S: RegisterInvoiceAsync(invoice, change, invId) + S->>DB: fds__createInvoice / fds__setInvoice + fds__createInvoice_Details + DB-->>S: registered invoice row (Id, ...) + S->>S: GenerateInvoicePdf (draft=true -> watermark, no GiroCode) + S-->>C: MigraDoc Document + C-->>JS: {id, img[] (base64 pages), total} + JS->>U: show PDF preview modal (confirm / cancel) +``` + +From the preview modal the user can loop back to editing (close and adjust), +**cancel** (deletes the draft via `req/sdel`), or **confirm** to finalise. + +## 5. Finalisation ("Rechnung erstellen") + +Confirming the preview modal posts to **`req/sconf`** +(`HandleRequestSconf`): + +1. `EXEC fds__setInvoiceFinal @Id, @authuser` — guarded by + `isFinal = 0 AND isSent = 0` so it can only fire once per draft. This is + also where the **real, sequential invoice number** is assigned: + `[InvoiceId] = fds__newInvoiceId(YEAR())` → format `R-<0000>` + (the draft only ever had the internal numeric `Id`). `Version` is bumped + and any linked "replaces" bookkeeping (storno/credit chains) is updated. +2. On success (`IsFinal == true` returned), the controller: + - Reloads the invoice via `LoadInvoiceAsync`. + - Calls `StoreInvoiceDocumentFileAsync` → renders the **final** PDF + (`draft` now reflects `IsFinal`, so the watermark disappears and the + GiroCode payment QR is added) → `fds__setInvoiceFile` persists the PDF + bytes on the invoice row → `IBlobStorageService.UploadInvoicePdfAsync` + archives a copy to blob storage. + - Re-fetches the invoice (`fds__getInvoice`) to get the final + `SendToEmail` / `DocumentName` / `InvoiceBalance`. +3. If a recipient email is present and the PDF rendered, the controller emails + it immediately (see §6) and marks the invoice `Sent` via + `fds__setInvoiceSent @auto=true`. +4. The browser opens the stored PDF in a new tab (`req/idoc`), returns to the + request list, and reloads it (`$ocms.init('req')`, `$inv.rReload()`). + +```mermaid +flowchart TD + A[POST req/sconf] --> B{fds__setInvoiceFinal\nisFinal=0 AND isSent=0?} + B -- no --> Z[500 - Aktion war nicht erfolgreich] + B -- yes --> C[Assign real InvoiceId\nR-yyyy-nnnn, bump Version] + C --> D[LoadInvoiceAsync] + D --> E[Render final PDF\n[no watermark, + GiroCode]] + E --> F[fds__setInvoiceFile\npersist PDF bytes] + F --> G[Upload to blob storage\nIBlobStorageService] + G --> H{SendToEmail set\nand PDF non-empty?} + H -- yes --> I[IComService.SendEmailAsync\nattach PDF inline] + I --> J[fds__setInvoiceSent auto=true] + H -- no --> K[Skip email - PDF only] + J --> L[Open PDF in new tab / reload request list] + K --> L +``` + +## 6. Sending the invoice by email + +Email delivery is handled by `IComService` / `ProcessWebComService`, which +talks to the **ProcessWeb Mailer API** (`push_com`, `comType=email`). + +- `HandleRequestSconf` builds the HTML body via `BuildInvoiceBody(balance, + paymentTerms)` — a fixed German thank-you text, the amount (if non-zero), + and payment instructions (IBAN/BIC) with the invoice's payment term + (`fdInv.PaymentTerms`, e.g. `10wd` → "10 Werktagen"). +- The rendered PDF bytes are attached as `{ [DocumentName] = filebyte }` — a + single-entry dictionary of filename → bytes. +- `SendEmailAsync(reference, subject, htmlBody, recipient, displayName, + attachments)` is called with `reference = "inv_"`, + `subject = "SanitärFuchs - "`. +- Inside `ProcessWebComService.SendEmailAsync`: + - The recipient email is validated; a configured + `Fuchs:Email:OverrideRecipient` (dev/test safety net, must stay empty in + Production) redirects **all** outbound mail to a single address. + - A signature is appended to the HTML body. + - If the mailer is disabled (`_settings.Enabled == false`), sending is a + documented no-op. + - Attachments are base64-encoded inline (`{ filename, mimeType, + contentBase64 }`) and POSTed to the Mailer API. +- Only on a **successful** send does the controller call + `fds__setInvoiceSent @auto=true`, which sets `IsSent = 1` — so a failed + mailer call leaves the invoice finalised but *not marked sent* (visible via + the "sis" — "als versendet markieren" — manual action, see §7). + +```mermaid +sequenceDiagram + participant C as IntranetController + participant Com as IComService (ProcessWebComService) + participant API as ProcessWeb Mailer API + participant DB as SQL + + C->>C: BuildInvoiceBody(balance, paymentTerms) + C->>Com: SendEmailAsync("inv_", subject, body, email, "", {pdf}) + Com->>Com: validate email, apply OverrideRecipient (dev), append signature + alt mailer enabled + Com->>API: POST push_com {comType:"email", attachments:[base64 pdf]} + API-->>Com: success/failure + else mailer disabled + Com-->>Com: no-op (logged) + end + Com-->>C: sent: true/false + opt sent == true + C->>DB: EXEC fds__setInvoiceSent @auto=true + end +``` + +## 7. After finalisation — manual actions + +Once an invoice is `IsFinal`, the request/invoice list menus +(`$inv.iMn`/`$inv.iMnr` in `fis.inv_shared.js`) expose: + +| Action | JS | Endpoint | Notes | +|---|---|---|---| +| **Resend email** | `$inv.ccInv`/context menu → *(re-)send* → `HandleRequestResend` | `req/resend` | Re-renders the stored/registered PDF bytes (`RenderInvoicePdfBytesAsync`, no re-registration) and re-sends via `IComService`, **without** re-touching `IsSent` | +| **Mark as sent manually** | `$inv.sis(id)` | `inv/sis` | `fds__setInvoiceSent @auto=false` — for invoices sent outside the system (e.g. printed/posted) | +| **View / display PDF** | `$inv.disp(id, 'inv')` | `inv/rdoc` | Renders page images for on-screen display only | +| **Download / open PDF** | `req/idoc` or `inv/pget`-derived | `req/idoc` | Serves the stored (or freshly rendered) PDF inline | +| **Mark paid / unpaid** | `$inv.setPyd`/`setUpd` | `inv/setpyd` / `inv/setupd` | `fds__setInvoicePayed` / `fds__setInvoiceUNPayed` | +| **Storno (cancel)** | `$inv.storno` → `$inv.cSt` | `inv/storno` (mode `simple`/`copy`) | `fds__createStorno_simple` / `fds__createStorno_copy`; the replaced invoice is cross-linked (`Replaces_InvId`) and cancelled once the storno itself is finalised | +| **Credit note** | `$inv.credit` → `$inv.cSt` | `inv/credit` (mode `credit`) | `fds__createCredit_simple` — same preview/finalise/email loop as a normal invoice | +| **Continue editing a draft** | `$inv.cntInv` / `clCntInv` | `inv/get` | Only available while `isFinal == false`; re-opens the same stateless editor | + +Storno/credit invoices are **new invoice drafts** created from the original +one — they go through the exact same preview → finalise → email pipeline +described in §4–§6, just seeded from `fds__createStorno_*`/`fds__createCredit_simple` +instead of from a service request. + +## 8. Reminders (Mahnungen) — downstream of a sent, unpaid invoice + +Not part of the invoice creation flow itself, but the natural continuation +once an invoice is sent and remains unpaid: + +- `$inv.ccRem(id, InvoiceId)` → `rem/lrem` (load prior reminder history) → + reminder editor (`$inv.ccRem_s2`) → `rem/get` → **preview** via `rem/prep` + (`$inv.rprev`, same stateless pattern as invoice preview) → **finalise** + via `rem/conf` → PDF opened, list reloaded. +- `$inv.dspRem(id)` lists existing reminders (`inv/getrem`); each can be + resent (`rem/resend`, mirrors `req/resend`) or downloaded. +- `$inv.srs(id)` → `rem/srs` marks a reminder as sent manually (mirrors + `inv/sis`). +- Reminder PDFs use the same draft-watermark/GiroCode rule as invoices + (`FuchsPdf.ApplyReminder`, `rem.IsDraft`). + +This is handled by `IReminderService`/`ReminderService` and +`IntranetController.Reminder.cs`, following the identical +draft-preview-finalise-email shape as invoices — intentionally, so the two +flows share the same mental model for the office user. + +## 9. End-to-end summary diagram + +```mermaid +flowchart TD + subgraph Requests + R1[Service request completed] --> R2[Open request list - req/reql] + R2 --> R3[Select request(s), click invoice icon] + end + + subgraph Draft & Edit + R3 --> D1[req/get - load request + existing invoice] + D1 --> D2[Edit items, address, email,\nset-mode, §13b, contact] + D2 --> D3[req/save - persist draft only] + D2 --> D4[req/sprep or req/sedit -\nregister + render draft PDF] + D4 --> D5[Preview modal\ndraft watermark, no GiroCode] + D5 -->|edit more| D2 + D5 -->|cancel| D6[req/sdel - delete draft] + end + + subgraph Finalise & Send + D5 -->|confirm| F1[req/sconf -\nfds__setInvoiceFinal] + F1 --> F2[Assign real InvoiceId\nR-yyyy-nnnn] + F2 --> F3[Render final PDF\nno watermark, + GiroCode] + F3 --> F4[fds__setInvoiceFile +\nblob storage upload] + F4 --> F5{SendToEmail set?} + F5 -- yes --> F6[IComService.SendEmailAsync\nProcessWeb Mailer API] + F6 --> F7[fds__setInvoiceSent auto=true] + F5 -- no --> F8[PDF stored, not emailed] + end + + subgraph After Sending + F7 --> A1[req/resend - resend email] + F7 --> A2[inv/setpyd / setupd - payment status] + F7 --> A3[inv/storno or inv/credit -\nnew linked invoice draft] + F7 --> A4[rem/* - reminder / Mahnung flow] + A3 -.re-enters.-> D2 + end +``` + +## 10. Key invariants worth remembering + +- **Stateless editor**: every preview/save/finalise call re-posts the full + `invc` JSON; the server never holds a partial invoice in memory or session + between requests (see `EVAL_live_invoice_editing.md`). +- **Totals come from the registration, not the rendered lines**: `sms.ttn` + /`sms.ttb` (posted) become `InvoiceBalance`/`InvoiceBalance_net`; display + mode (set pricing) never changes what the customer owes. +- **Numbering happens only at finalisation**: the draft has an internal `Id` + but no `InvoiceId` until `fds__setInvoiceFinal` assigns + `R-` — so previews never "burn" an invoice number. +- **Draft vs. final changes the rendered PDF**: draft = watermark overlay, no + GiroCode; final = no watermark, GiroCode payment QR added when there's a + positive balance. +- **Email is best-effort and tracked**: `IsSent` is only set `true` + automatically after a *successful* send; a failed send still leaves a + correctly finalised, stored invoice that staff can resend or mark sent + manually. +- **Storno/credit/reminder all reuse the same pipeline**: they are not special + cases in the UI/backend contract — they are just differently-seeded drafts + going through the identical preview → finalise → email sequence. +5 \ No newline at end of file diff --git a/Fuchs/Fuchs.csproj b/Fuchs/Fuchs.csproj index c0299e6..4207fe6 100644 --- a/Fuchs/Fuchs.csproj +++ b/Fuchs/Fuchs.csproj @@ -34,12 +34,12 @@ - - - - + + + + - + @@ -51,10 +51,11 @@ - + - - + + +
diff --git a/Fuchs/Observability/FuchsTelemetry.cs b/Fuchs/Observability/FuchsTelemetry.cs index 8b8027f..2920d33 100644 --- a/Fuchs/Observability/FuchsTelemetry.cs +++ b/Fuchs/Observability/FuchsTelemetry.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.Metrics; namespace Fuchs.Observability; @@ -41,6 +41,10 @@ public static class FuchsTelemetry Meter.CreateCounter("fuchs.banking.mt940.rows", "{row}", "Number of MT940 transaction lines parsed."); public static readonly Counter MfrCalls = Meter.CreateCounter("fuchs.mfr.calls", "{call}", "Number of MFR ERP client calls initiated."); + public static readonly Counter BlobUploadsSucceeded = + Meter.CreateCounter("fuchs.blobstorage.uploads", "{upload}", "Number of documents successfully archived to Azure Blob Storage."); + public static readonly Counter BlobUploadsFailed = + Meter.CreateCounter("fuchs.blobstorage.uploads.failed", "{upload}", "Number of documents that failed to archive to Azure Blob Storage."); // ── Performance histograms (durations in milliseconds) ─────────────────── public static readonly Histogram PdfRenderDuration = diff --git a/Fuchs/Program.cs b/Fuchs/Program.cs index d457563..3003852 100644 --- a/Fuchs/Program.cs +++ b/Fuchs/Program.cs @@ -36,8 +36,10 @@ public class Program // Key Vault + DPAPI secret management (must run before FuchsOcmsIntranet.Initialize) builder.AddSecretManagement(); - // Assemble connection strings from templates + resolved credentials - AssembleConnectionStrings(builder.Configuration); + // Assemble connection strings from templates + resolved credentials. + // In Development, "_Dev"-suffixed credential keys are preferred so a reachable + // Key Vault can never override them with production DB credentials. + AssembleConnectionStrings(builder.Configuration, builder.Environment); // Initialize the Fuchs intranet singleton with configuration FuchsOcmsIntranet.Initialize(builder.Configuration); @@ -80,6 +82,9 @@ public class Program // Communication service (email + SMS via ProcessWeb Mailer API) builder.Services.Configure(builder.Configuration.GetSection("Fuchs:Mailer")); + // Dev/test safety net: Fuchs:Email:OverrideRecipient redirects every outbound email + // (see appsettings.Development.json) so real tenant-owners/end-customers are never emailed. + builder.Services.Configure(builder.Configuration.GetSection("Fuchs:Email")); builder.Services.AddHttpClient("ProcessWebMailer"); builder.Services.AddScoped(); @@ -92,6 +97,15 @@ public class Program builder.Services.AddScoped(); builder.Services.AddScoped(); + // Secondary archive: invoice/reminder PDFs additionally stored in Azure Blob Storage. + // Disabled by default (Fuchs:AzureStorage:Enabled) — see AzureBlobStorageService. + builder.Services.Configure(builder.Configuration.GetSection("Fuchs:AzureStorage")); + builder.Services.AddSingleton(); + + // One-shot startup backfill: archives invoices/reminders that already had a file in SQL + // before Blob Storage archiving was enabled. No-ops when Fuchs:AzureStorage:Enabled is false. + builder.Services.AddHostedService(); + // ── OpenTelemetry: tracing + metrics ───────────────────────────────── // Instrumentation is always collected; OTLP export is enabled only when // an endpoint is configured (Fuchs:Telemetry:OtlpEndpoint), so a missing @@ -172,15 +186,18 @@ public class Program /// resolved credential secrets, then overrides the config entries in-place. /// When appsettings.Development.json supplies a complete connection string (no tokens), /// the replace is a no-op and the original value is preserved. + /// In Development, "{key}_Dev" credential keys are tried first. These are never populated + /// by Key Vault (no matching ManagedSecretKeys entry exists for them), so a developer whose + /// machine happens to reach the shared Key Vault can never have production DB credentials + /// silently override their local appsettings.Development.json values. /// - private static void AssembleConnectionStrings(ConfigurationManager config) + private static void AssembleConnectionStrings(ConfigurationManager config, IWebHostEnvironment environment) { const string userToken = "{username}"; const string passToken = "{password}"; (string csName, string userKey, string passKey)[] pairs = [ - ("ocms_ConnectionString", "ConnectionStrings:ocms_username", "ConnectionStrings:ocms_password"), ("fuchs_fds_ConnectionString", "ConnectionStrings:fuchs_fds_username", "ConnectionStrings:fuchs_fds_password"), ]; @@ -192,8 +209,8 @@ public class Program if (!template.Contains(userToken, StringComparison.Ordinal) && !template.Contains(passToken, StringComparison.Ordinal)) continue; - var user = config[userKey] ?? ""; - var pass = config[passKey] ?? ""; + var user = (environment.IsDevelopment() ? config[$"{userKey}_Dev"] : null) ?? config[userKey] ?? ""; + var pass = (environment.IsDevelopment() ? config[$"{passKey}_Dev"] : null) ?? config[passKey] ?? ""; overrides[$"ConnectionStrings:{csName}"] = template .Replace(userToken, user, StringComparison.Ordinal) .Replace(passToken, pass, StringComparison.Ordinal); diff --git a/Fuchs/Services/AzureBlobStorageService.cs b/Fuchs/Services/AzureBlobStorageService.cs new file mode 100644 index 0000000..f497f2e --- /dev/null +++ b/Fuchs/Services/AzureBlobStorageService.cs @@ -0,0 +1,147 @@ +using System.Diagnostics; +using Azure; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Fuchs.Observability; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Fuchs.Services; + +/// +/// Archives finalized invoice/reminder PDFs (and, via , any +/// future file-bearing document type) to Azure Blob Storage, in addition to the existing SQL +/// Server storage (see and ). This is +/// a best-effort secondary archive: when is +/// false (default) or no connection string is configured, uploads are skipped and only +/// logged; upload failures are caught and logged rather than propagated, so a missing or +/// unreachable storage account never breaks invoice/reminder finalization. +/// +public class AzureBlobStorageService : IBlobStorageService +{ + private readonly ILogger _logger; + private readonly AzureBlobStorageSettings _settings; + private readonly BlobServiceClient? _client; + + public AzureBlobStorageService(IConfiguration configuration, + IOptions settings, + ILogger logger) + { + _logger = logger; + _settings = settings.Value; + + if (_settings.Enabled) + { + string? connectionString = configuration.GetConnectionString("AzureBlobStorage_ConnectionString"); + if (!string.IsNullOrWhiteSpace(connectionString) && connectionString != "MANAGED_BY_KEYVAULT") + { + _client = new BlobServiceClient(connectionString); + } + else + { + _logger.LogWarning( + "AzureBlobStorageService is enabled but ConnectionStrings:AzureBlobStorage_ConnectionString " + + "is not configured — uploads will be skipped."); + } + } + } + + /// Test-only constructor allowing an already-built (typically mocked) client to be injected. + internal AzureBlobStorageService(BlobServiceClient? client, AzureBlobStorageSettings settings, + ILogger logger) + { + _client = client; + _settings = settings; + _logger = logger; + } + + public Task UploadInvoicePdfAsync(string invoiceId, string fileName, byte[] content, + IReadOnlyDictionary? sourceRow = null, CancellationToken cancellationToken = default) + => UploadAsync("invoice", _settings.InvoiceContainer, invoiceId, fileName, content, sourceRow, cancellationToken); + + public Task UploadReminderPdfAsync(string reminderId, string fileName, byte[] content, + IReadOnlyDictionary? sourceRow = null, CancellationToken cancellationToken = default) + => UploadAsync("reminder", _settings.ReminderContainer, reminderId, fileName, content, sourceRow, cancellationToken); + + public Task UploadDocumentAsync(string category, string containerName, string documentId, string fileName, + byte[] content, IReadOnlyDictionary? sourceRow = null, CancellationToken cancellationToken = default) + => UploadAsync(category, containerName, documentId, fileName, content, sourceRow, cancellationToken); + + public async Task ExistsAsync(string containerName, string documentId, string fileName, + CancellationToken cancellationToken = default) + { + if (_client == null) return false; + + string blobName = BuildBlobName(documentId, fileName); + try + { + var containerClient = _client.GetBlobContainerClient(containerName); + var blobClient = containerClient.GetBlobClient(blobName); + Response response = await blobClient.ExistsAsync(cancellationToken); + return response.Value; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Blob existence check failed for {Container}/{Blob} — treating as not archived.", + containerName, blobName); + return false; + } + } + + private static string BuildBlobName(string documentId, string fileName) => + $"{documentId}/{(string.IsNullOrWhiteSpace(fileName) ? $"{documentId}.pdf" : fileName)}"; + + private async Task UploadAsync(string category, string containerName, string documentId, + string fileName, byte[] content, IReadOnlyDictionary? sourceRow, CancellationToken cancellationToken) + { + if (_client == null) + { + _logger.LogDebug("Blob upload skipped for {Category} {Id} — storage disabled/unconfigured.", category, documentId); + return null; + } + if (content.Length == 0) + { + _logger.LogWarning("Blob upload skipped for {Category} {Id} — empty content.", category, documentId); + return null; + } + + using var act = FuchsTelemetry.StartActivity("blobstorage.upload"); + act?.SetTag("fuchs.blobstorage.category", category); + act?.SetTag("fuchs.blobstorage.id", documentId); + string blobName = BuildBlobName(documentId, fileName); + Dictionary? metadata = sourceRow != null + ? DocumentMetadataBuilder.Build(sourceRow, _settings.MetadataFields) + : null; + + try + { + var containerClient = _client.GetBlobContainerClient(containerName); + await containerClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken); + var blobClient = containerClient.GetBlobClient(blobName); + using var stream = new MemoryStream(content, writable: false); + if (metadata is { Count: > 0 }) + { + var options = new BlobUploadOptions { Metadata = metadata }; + await blobClient.UploadAsync(stream, options, cancellationToken); + } + else + { + await blobClient.UploadAsync(stream, overwrite: true, cancellationToken); + } + + FuchsTelemetry.BlobUploadsSucceeded.Add(1, new KeyValuePair("category", category)); + _logger.LogInformation("Uploaded {Category} {Id} to container {Container} as {Blob}.", + category, documentId, containerName, blobName); + return blobClient.Uri; + } + catch (Exception ex) + { + FuchsTelemetry.BlobUploadsFailed.Add(1, new KeyValuePair("category", category)); + act?.SetStatus(ActivityStatusCode.Error, ex.Message); + _logger.LogError(ex, "Blob upload failed for {Category} {Id} in container {Container}.", + category, documentId, containerName); + return null; + } + } +} diff --git a/Fuchs/Services/AzureBlobStorageSettings.cs b/Fuchs/Services/AzureBlobStorageSettings.cs new file mode 100644 index 0000000..d4c6b96 --- /dev/null +++ b/Fuchs/Services/AzureBlobStorageSettings.cs @@ -0,0 +1,33 @@ +namespace Fuchs.Services; + +/// +/// Azure Blob Storage settings, bound from appsettings.json → "Fuchs:AzureStorage". +/// The storage account connection string itself is a secret and therefore lives +/// under the standard ConnectionStrings key (see , +/// which reads it via IConfiguration.GetConnectionString("AzureBlobStorage_ConnectionString")) +/// instead of being bound here. +/// +public class AzureBlobStorageSettings +{ + /// + /// When false (default) blob uploads are skipped and only logged, so the + /// feature is opt-in and never impacts environments that haven't configured a + /// storage account + Key Vault secret yet. Set to true to enable archiving. + /// + public bool Enabled { get; set; } = false; + + /// Blob container that stores finalized invoice PDFs. + public string InvoiceContainer { get; set; } = "fuchs-invoices"; + + /// Blob container that stores finalized reminder PDFs. + public string ReminderContainer { get; set; } = "fuchs-reminders"; + + /// + /// Column/property names considered when building the per-blob metadata dictionary + /// (see ). Not every document type has every + /// column: fields absent from a given source row are skipped entirely, while fields + /// that are present but hold an empty value are still stored as an empty string. + /// + public List MetadataFields { get; set; } = + new() { "Id", "Version", "InvoiceId", "InvoiceTitle", "InvId", "DocumentName", "file_guid" }; +} diff --git a/Fuchs/Services/DocumentArchiveSyncService.cs b/Fuchs/Services/DocumentArchiveSyncService.cs new file mode 100644 index 0000000..5174fcd --- /dev/null +++ b/Fuchs/Services/DocumentArchiveSyncService.cs @@ -0,0 +1,157 @@ +using System.Data; +using Fuchs.intranet; +using Fuchs.Observability; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using static OCORE.commons; +using static OCORE.SQL.sql; + +namespace Fuchs.Services; + +/// +/// Startup backfill for the Azure Blob Storage secondary archive (see ). +/// When is true, this one-shot background task +/// enumerates every invoice/reminder that already has a file stored in SQL Server +/// (fds__getInvoiceFiles_ForBlobArchive / fds__getReminderFiles_ForBlobArchive), skips +/// documents already archived (), and uploads the rest — +/// fetching bytes lazily via fds__getInvoiceFileContent / fds__getReminderFileContent so the +/// enumeration query itself stays lightweight (no VARBINARY column). New invoices/reminders created after +/// startup are archived inline by / ; this service +/// only covers historical documents that predate the feature being enabled. +/// Runs once at startup (not periodic) and never throws: failures are logged so a database or storage +/// hiccup during startup can never prevent the app from serving requests. +/// +public class DocumentArchiveSyncService : BackgroundService +{ + private const int BackfillConcurrency = 4; + + private readonly Fuchs_intranet _intranet; + private readonly IBlobStorageService _blobStorage; + private readonly AzureBlobStorageSettings _settings; + private readonly ILogger _logger; + + public DocumentArchiveSyncService(Fuchs_intranet intranet, IBlobStorageService blobStorage, + IOptions settings, ILogger logger) + { + _intranet = intranet; + _blobStorage = blobStorage; + _settings = settings.Value; + _logger = logger; + } + + private string Conn => _intranet.Intranet__SQLConnectionString; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!_settings.Enabled) + { + _logger.LogDebug("DocumentArchiveSyncService skipped — Fuchs:AzureStorage:Enabled is false."); + return; + } + + using var act = FuchsTelemetry.StartActivity("blobstorage.backfill"); + _logger.LogInformation("DocumentArchiveSyncService starting startup backfill."); + try + { + int invoices = await SyncInvoicesAsync(stoppingToken); + int reminders = await SyncRemindersAsync(stoppingToken); + _logger.LogInformation( + "DocumentArchiveSyncService completed: {Invoices} invoice(s), {Reminders} reminder(s) newly archived.", + invoices, reminders); + } + catch (OperationCanceledException) + { + _logger.LogWarning("DocumentArchiveSyncService backfill cancelled (application shutting down)."); + } + catch (Exception ex) + { + _logger.LogError(ex, "DocumentArchiveSyncService backfill failed."); + } + } + + private async Task SyncInvoicesAsync(CancellationToken cancellationToken) + { + var dt = await getSQLDatatable_async( + "EXECUTE [dbo].[fds__getInvoiceFiles_ForBlobArchive];", + Conn, Security: _intranet.GetDbSecurity(), options: new FIS_SQLOptions()); + if (dt.Count == 0) return 0; + + int archived = 0; + var rows = dt.DataTable.Rows.Cast().ToList(); + await Parallel.ForEachAsync(rows, + new ParallelOptions { MaxDegreeOfParallelism = BackfillConcurrency, CancellationToken = cancellationToken }, + async (row, ct) => + { + string id = row.nz("Id"); + if (string.IsNullOrEmpty(id)) return; + try + { + string fileName = row.nz("DocumentName").ne($"Rechnung_{id}.pdf"); + if (await _blobStorage.ExistsAsync(_settings.InvoiceContainer, id, fileName, ct)) + return; + + byte[]? content = await GetFileContentAsync( + "EXECUTE [dbo].[fds__getInvoiceFileContent] @Id;", id); + if (content is not { Length: > 0 }) return; + + var uri = await _blobStorage.UploadInvoicePdfAsync( + id, fileName, content, row.toObjectDictionary(), ct); + if (uri != null) Interlocked.Increment(ref archived); + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + _logger.LogWarning(ex, "Invoice backfill failed for {Id} — skipping.", id); + } + }); + return archived; + } + + private async Task SyncRemindersAsync(CancellationToken cancellationToken) + { + var dt = await getSQLDatatable_async( + "EXECUTE [dbo].[fds__getReminderFiles_ForBlobArchive];", + Conn, Security: _intranet.GetDbSecurity(), options: new FIS_SQLOptions()); + if (dt.Count == 0) return 0; + + int archived = 0; + var rows = dt.DataTable.Rows.Cast().ToList(); + await Parallel.ForEachAsync(rows, + new ParallelOptions { MaxDegreeOfParallelism = BackfillConcurrency, CancellationToken = cancellationToken }, + async (row, ct) => + { + string id = row.nz("Id"); + if (string.IsNullOrEmpty(id)) return; + try + { + string fileName = row.nz("DocumentName").ne($"Zahlungserinnerung_{id}.pdf"); + if (await _blobStorage.ExistsAsync(_settings.ReminderContainer, id, fileName, ct)) + return; + + byte[]? content = await GetFileContentAsync( + "EXECUTE [dbo].[fds__getReminderFileContent] @Id;", id); + if (content is not { Length: > 0 }) return; + + var uri = await _blobStorage.UploadReminderPdfAsync( + id, fileName, content, row.toObjectDictionary(), ct); + if (uri != null) Interlocked.Increment(ref archived); + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + _logger.LogWarning(ex, "Reminder backfill failed for {Id} — skipping.", id); + } + }); + return archived; + } + + private async Task GetFileContentAsync(string sql, string id) + { + var pl = new List { SQL_VarChar("@Id", id) }; + var dt = await getSQLDatatable_async(sql, Conn, pl, + Security: _intranet.GetDbSecurity(), options: new FIS_SQLOptions()); + return dt.Count > 0 ? dt.FirstRow.no("file", null) as byte[] : null; + } +} diff --git a/Fuchs/Services/DocumentMetadataBuilder.cs b/Fuchs/Services/DocumentMetadataBuilder.cs new file mode 100644 index 0000000..2c04a0e --- /dev/null +++ b/Fuchs/Services/DocumentMetadataBuilder.cs @@ -0,0 +1,58 @@ +namespace Fuchs.Services; + +/// +/// Builds the per-blob metadata dictionary used when archiving documents (invoice/reminder PDFs, +/// and any future file-bearing type) to Azure Blob Storage — see +/// and . +/// Only fields configured in that are ALSO +/// present as a key on the source row are included: a field absent from a given document type's +/// row shape (e.g. reminders have no file_guid) is skipped entirely, while a field that is +/// present but holds a null/empty value is still emitted as an empty string. +/// +public static class DocumentMetadataBuilder +{ + /// + /// Projects onto . Column lookup is + /// case-insensitive because SQL-sourced rows (see toObjectDictionary / + /// GenericObjectDictionary) are frequently lower-cased. + /// + public static Dictionary Build(IReadOnlyDictionary row, IEnumerable fields) + { + var metadata = new Dictionary(); + foreach (string field in fields) + { + if (string.IsNullOrWhiteSpace(field)) continue; + if (!TryGetValue(row, field, out object? value)) continue; + metadata[field] = Stringify(value); + } + return metadata; + } + + private static bool TryGetValue(IReadOnlyDictionary row, string field, out object? value) + { + if (row.TryGetValue(field, out value)) return true; + + // Fall back to a case-insensitive match: rows built from SQL results are frequently + // lower-cased (see toObjectDictionary/GenericObjectDictionary) while MetadataFields + // entries are written using the column's natural casing (e.g. "InvoiceId"). + foreach (var kvp in row) + { + if (string.Equals(kvp.Key, field, StringComparison.OrdinalIgnoreCase)) + { + value = kvp.Value; + return true; + } + } + value = null; + return false; + } + + private static string Stringify(object? value) => + value switch + { + null => "", + DBNull => "", + DateTime dt => dt.ToString("O"), + _ => value.ToString() ?? "" + }; +} diff --git a/Fuchs/Services/FuchsEmailSettings.cs b/Fuchs/Services/FuchsEmailSettings.cs new file mode 100644 index 0000000..117818d --- /dev/null +++ b/Fuchs/Services/FuchsEmailSettings.cs @@ -0,0 +1,16 @@ +namespace Fuchs.Services; + +/// +/// Email safety-net settings, bound from appsettings.json → "Fuchs:Email". +/// +public class FuchsEmailSettings +{ + /// + /// Dev/test safety net: when set to a non-empty address, + /// discards the real recipient of every outbound email (to/cc/bcc) and redirects it to this + /// single address instead, so a locally-enabled mailer can never reach a real tenant-owner or + /// end-customer while testing. Configure this only in appsettings.Development.json — + /// it must stay empty/unset in Production. + /// + public string? OverrideRecipient { get; set; } +} diff --git a/Fuchs/Services/FuchsWidgetService.cs b/Fuchs/Services/FuchsWidgetService.cs index e645b0e..a78ac49 100644 --- a/Fuchs/Services/FuchsWidgetService.cs +++ b/Fuchs/Services/FuchsWidgetService.cs @@ -1,4 +1,4 @@ -using Fuchs.intranet; +using Fuchs.intranet; using Fuchs.Observability; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -124,7 +124,8 @@ public class FuchsWidgetService : IWidgetService { case "sql_table": { - var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec); + var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec, + options: new FIS_SQLOptions { CommandTimeout = 90 }); widgetData = new { name, @@ -141,7 +142,8 @@ public class FuchsWidgetService : IWidgetService case "sql_indicator": { - var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec); + var dt = await getSQLDatatable_async(sql, Conn, Params(userAccountId), Security: dbSec, + options: new FIS_SQLOptions { CommandTimeout = 90 }); var firstRow = dt.DataTable.Rows.Count > 0 ? dt.DataTable.Rows[0].toObjectDictionary() : new Dictionary(); diff --git a/Fuchs/Services/IBlobStorageService.cs b/Fuchs/Services/IBlobStorageService.cs new file mode 100644 index 0000000..e33be9b --- /dev/null +++ b/Fuchs/Services/IBlobStorageService.cs @@ -0,0 +1,47 @@ +namespace Fuchs.Services; + +/// +/// Abstraction for archiving finalized documents (invoice/reminder PDFs, and any future +/// file-bearing type) to Azure Blob Storage, in addition to the existing SQL Server storage +/// (fds__setInvoiceFile / fds__setReminderFile). +/// +public interface IBlobStorageService +{ + /// + /// Uploads a finalized invoice PDF to Azure Blob Storage. When is + /// supplied (typically FdsInvoiceData.InvoiceRegistration), blob metadata is projected from + /// it using — see . + /// Returns the blob URI, or null when storage is disabled/unconfigured + /// or the upload failed — failures never break the primary DB-storage flow. + /// + Task UploadInvoicePdfAsync(string invoiceId, string fileName, byte[] content, + IReadOnlyDictionary? sourceRow = null, CancellationToken cancellationToken = default); + + /// + /// Uploads a finalized reminder PDF to Azure Blob Storage. When is + /// supplied (typically FdsReminderData.ReminderRegistration), blob metadata is projected from + /// it using — see . + /// Returns the blob URI, or null when storage is disabled/unconfigured + /// or the upload failed — failures never break the primary DB-storage flow. + /// + Task UploadReminderPdfAsync(string reminderId, string fileName, byte[] content, + IReadOnlyDictionary? sourceRow = null, CancellationToken cancellationToken = default); + + /// + /// Generic upload for any document category — used by the startup archive backfill so + /// invoice/reminder/future file types can all be archived through one entry point. The + /// caller supplies the target container name and a category label (used for logging/telemetry), + /// plus the source row driving metadata projection (see ). + /// Returns the blob URI, or null when storage is disabled/unconfigured or the upload failed. + /// + Task UploadDocumentAsync(string category, string containerName, string documentId, string fileName, + byte[] content, IReadOnlyDictionary? sourceRow = null, CancellationToken cancellationToken = default); + + /// + /// Returns true if a blob already exists for the given container/document/filename + /// combination. Used by the startup backfill to skip documents that were already archived. + /// Returns false (never throws) when storage is disabled/unconfigured or the check fails. + /// + Task ExistsAsync(string containerName, string documentId, string fileName, + CancellationToken cancellationToken = default); +} diff --git a/Fuchs/Services/InvoiceService.cs b/Fuchs/Services/InvoiceService.cs index 2808515..7433436 100644 --- a/Fuchs/Services/InvoiceService.cs +++ b/Fuchs/Services/InvoiceService.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using System.Diagnostics; using Fuchs.intranet; using Fuchs.Observability; @@ -22,12 +22,15 @@ public class InvoiceService : IInvoiceService { private readonly Fuchs_intranet _intranet; private readonly IPdfService _pdf; + private readonly IBlobStorageService _blobStorage; private readonly ILogger _logger; - public InvoiceService(Fuchs_intranet intranet, IPdfService pdf, ILogger logger) + public InvoiceService(Fuchs_intranet intranet, IPdfService pdf, IBlobStorageService blobStorage, + ILogger logger) { _intranet = intranet; _pdf = pdf; + _blobStorage = blobStorage; _logger = logger; } @@ -142,7 +145,12 @@ public class InvoiceService : IInvoiceService bool r = await setSQLValue_async( "EXECUTE [dbo].[fds__setInvoiceFile] @Id, @file;", Conn, pl, Security: dbSec, options: new FIS_SQLOptions()); - return r ? ba : Array.Empty(); + if (!r) return Array.Empty(); + + string fileName = invoice.InvoiceRegistration?.getString("DocumentName") + .ne($"Rechnung_{invoice.Id}.pdf") ?? $"Rechnung_{invoice.Id}.pdf"; + await _blobStorage.UploadInvoicePdfAsync(invoice.Id, fileName, ba, invoice.InvoiceRegistration); + return ba; } public async Task GetInvoiceFileAsync(FdsInvoiceData invoice, bool draft, fds.IFdsMfr mfr) diff --git a/Fuchs/Services/ProcessWebComService.cs b/Fuchs/Services/ProcessWebComService.cs index 6c5183c..2f1f658 100644 --- a/Fuchs/Services/ProcessWebComService.cs +++ b/Fuchs/Services/ProcessWebComService.cs @@ -23,6 +23,7 @@ public class ProcessWebComService : IComService private readonly ILogger _logger; private readonly Fuchs_intranet _intranet; private readonly ProcessWebComSettings _settings; + private readonly FuchsEmailSettings _emailSettings; private readonly IHttpClientFactory _httpClientFactory; private const string SignatureIntro = @@ -34,11 +35,13 @@ public class ProcessWebComService : IComService ILogger logger, Fuchs_intranet intranet, IOptions settings, + IOptions emailSettings, IHttpClientFactory httpClientFactory) { _logger = logger; _intranet = intranet; _settings = settings.Value; + _emailSettings = emailSettings.Value; _httpClientFactory = httpClientFactory; } @@ -47,6 +50,22 @@ public class ProcessWebComService : IComService { using var act = FuchsTelemetry.StartActivity("email.send"); act?.SetTag("fuchs.email.ref", reference); + + string overrideRecipient = _emailSettings.OverrideRecipient ?? ""; + if (!string.IsNullOrWhiteSpace(overrideRecipient)) + { + // Dev/test safety net: discard the real recipient (to/cc/bcc) entirely and + // redirect every outbound email to a single controlled inbox, so a locally + // enabled mailer can never reach a real tenant-owner or end-customer. + _logger.LogWarning( + "SendEmailAsync: recipient override active for ref {Reference} – redirecting from '{OriginalEmail}' to '{OverrideRecipient}'", + reference, email, overrideRecipient); + act?.SetTag("fuchs.email.overridden", true); + act?.SetTag("fuchs.email.original_recipient", email); + subject = $"[DEV \u2192 {email}] {subject}"; + email = overrideRecipient; + } + if (!IsValidEmail(email)) { _logger.LogWarning("SendEmailAsync: invalid email address '{Email}' for ref {Reference}", email, reference); diff --git a/Fuchs/Services/ReminderService.cs b/Fuchs/Services/ReminderService.cs index acebce4..1d03409 100644 --- a/Fuchs/Services/ReminderService.cs +++ b/Fuchs/Services/ReminderService.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using System.Diagnostics; using Fuchs.intranet; using Fuchs.Observability; @@ -22,12 +22,15 @@ public class ReminderService : IReminderService { private readonly Fuchs_intranet _intranet; private readonly IPdfService _pdf; + private readonly IBlobStorageService _blobStorage; private readonly ILogger _logger; - public ReminderService(Fuchs_intranet intranet, IPdfService pdf, ILogger logger) + public ReminderService(Fuchs_intranet intranet, IPdfService pdf, IBlobStorageService blobStorage, + ILogger logger) { _intranet = intranet; _pdf = pdf; + _blobStorage = blobStorage; _logger = logger; } @@ -145,7 +148,12 @@ public class ReminderService : IReminderService bool r = await setSQLValue_async( "EXECUTE [dbo].[fds__setReminderFile] @Id, @file;", Conn, pl, Security: dbSec, options: new FIS_SQLOptions()); - return r ? ba : Array.Empty(); + if (!r) return Array.Empty(); + + string fileName = reminder.ReminderRegistration?.getString("DocumentName") + .ne($"Zahlungserinnerung_{reminder.Id}.pdf") ?? $"Zahlungserinnerung_{reminder.Id}.pdf"; + await _blobStorage.UploadReminderPdfAsync(reminder.Id, fileName, ba, reminder.ReminderRegistration); + return ba; } public async Task GetReminderFileAsync(FdsReminderData reminder, bool draft, diff --git a/Fuchs/appsettings.Development.json b/Fuchs/appsettings.Development.json index 76ca4d1..73a182f 100644 --- a/Fuchs/appsettings.Development.json +++ b/Fuchs/appsettings.Development.json @@ -1,7 +1,8 @@ { "ConnectionStrings": { - "ocms_ConnectionString": "Server=localhost;Database=ocms;User Id=DEV_USERNAME;Password=DEV_PASSWORD;TrustServerCertificate=True;", - "fuchs_fds_ConnectionString": "Server=localhost;Database=fuchs_fds;User Id=DEV_USERNAME;Password=DEV_PASSWORD;TrustServerCertificate=True;" + "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={username};password='{password}';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;", + "fuchs_fds_username_Dev": "fuchs_dev", + "fuchs_fds_password_Dev": "!Po@cGZ5bUn37khO" }, "Logging": { "LogLevel": { @@ -12,9 +13,14 @@ "Fuchs": { "FDS_Intranet_DebugState": true, "DevAutoLogin": true, - "DevAutoLoginEmail": "your.email@example.com", + "DevAutoLoginEmail": "info@processweb.de", "Email": { - "DevRedirectAddress": "service@emails.processweb.de" + "OverrideRecipient": "service@emails.processweb.de" + }, + "AzureStorage": { + "Enabled": false, + "InvoiceContainer": "dev-fuchs-invoices", + "ReminderContainer": "dev-fuchs-reminders" } } } diff --git a/Fuchs/appsettings.json b/Fuchs/appsettings.json index d773025..930f754 100644 --- a/Fuchs/appsettings.json +++ b/Fuchs/appsettings.json @@ -5,10 +5,9 @@ "CacheFilePath": "secrets.cache", "SyncIntervalHours": 6, "ManagedSecretKeys": [ - "ConnectionStrings--ocms-username", - "ConnectionStrings--ocms-password", "ConnectionStrings--fuchs-fds-username", "ConnectionStrings--fuchs-fds-password", + "ConnectionStrings--AzureBlobStorage-ConnectionString", "Fuchs--SMS-APIKey", "Fuchs--Mailer--Token", "Fuchs--fuchs-captcha-TOTP", @@ -23,12 +22,10 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "ocms_ConnectionString": "Server=DB_SERVER;Database=ocms;User Id={username};Password={password};TrustServerCertificate=True;", - "fuchs_fds_ConnectionString": "Server=DB_SERVER;Database=fuchs_fds;User Id={username};Password={password};TrustServerCertificate=True;", - "ocms_username": "MANAGED_BY_KEYVAULT", - "ocms_password": "MANAGED_BY_KEYVAULT", + "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={username};password='{password}';Connect Timeout=60;Load Balance Timeout=240;Max Pool Size=500;", "fuchs_fds_username": "MANAGED_BY_KEYVAULT", - "fuchs_fds_password": "MANAGED_BY_KEYVAULT" + "fuchs_fds_password": "MANAGED_BY_KEYVAULT", + "AzureBlobStorage_ConnectionString": "MANAGED_BY_KEYVAULT" }, "Fuchs": { "ocms_guid": "00094b8f-a822-4e9c-b627-87802f93fca8", @@ -45,6 +42,15 @@ "Token": "MANAGED_BY_KEYVAULT", "Enabled": false }, + "Email": { + "OverrideRecipient": "" + }, + "AzureStorage": { + "Enabled": false, + "InvoiceContainer": "fuchs-invoices", + "ReminderContainer": "fuchs-reminders", + "MetadataFields": [ "Id", "Version", "InvoiceId", "InvoiceTitle", "InvId", "DocumentName", "file_guid" ] + }, "Telemetry": { "Enabled": true, "OtlpEndpoint": "" diff --git a/Fuchs/js/intranet/fis_main.js b/Fuchs/js/intranet/fis_main.js index 1747caa..b028006 100644 --- a/Fuchs/js/intranet/fis_main.js +++ b/Fuchs/js/intranet/fis_main.js @@ -150,7 +150,7 @@ $fis.resetPass = function (id, fds) { $fis.wdg = function (options) { let wf = $(this).empty(); $ocms.postXT({ - url: $ocms.url('wdg/one'), data: { short_name: options.wdg }, success: function (response, textStatus, jqXHR) { + url: $ocms.url('wdg/one'), data: { short_name: options.wdg }, timeout: 90000, success: function (response, textStatus, jqXHR) { let wi = options.wdg, wx = response[wi]; if (!wx) { wf.ldng(0); return; } let dbl = $.inArrayRegEx('dblwidth', wx.rendering_options) > -1, tiny = $.inArrayRegEx('tiny', wx.rendering_options) > -1; @@ -175,7 +175,7 @@ $fis.wdg = function (options) { var tdr = $$.tr().appendTo(tblset.bdy); $.each(wx.columns, function (ci, col) { var tdc = $$.td().appendTo(tdr); - if (dx[col] instanceof Date || $ocms.isDateString(dx[col]) === true) { + if (dx[col] instanceof Date || $ocms.isJSONDateString(dx[col]) === true) { tdc.text(fdt(dx[col], $t.dateformat)); } else { tdc.rwText(dx[col]); diff --git a/Fuchs/js/intranet/oci_basic.js b/Fuchs/js/intranet/oci_basic.js index 03bffe6..d826cd1 100644 --- a/Fuchs/js/intranet/oci_basic.js +++ b/Fuchs/js/intranet/oci_basic.js @@ -168,21 +168,34 @@ function ne(inp, alt) { return (inp || '') === '' ? (alt || '') : inp; } function pad(i, n) { return (i || '').toString().padStart(n, '0').substr(-1 * n); } +function twoDigitYear(y) { var n = parseInt(y, 10); return n + (n < 70 ? 2000 : 1900); } +/* Parses dot-separated German short dates (dd.MM.yyyy / dd.MM.yy, optionally with a time part). + Returns null if the string does not match, so callers can fall back to other parsing. */ +function parseGermanDate(si) { + var g = si.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2}|\d{4})(?:[\sT](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/); + if (g === null) { return null; } + var yr = g[3].length === 2 ? twoDigitYear(g[3]) : parseInt(g[3], 10); + return new Date(yr, parseInt(g[2], 10) - 1, parseInt(g[1], 10), parseInt(g[4] || '0', 10), parseInt(g[5] || '0', 10), parseInt(g[6] || '0', 10)); +} function parseISO(s) { let si = s || ''; if (si === '') { return null }; if (/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/.test(si) === true) { return new Date(si); } else { - var b = s.split(/\D/); - return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); + var gd = parseGermanDate(si); + if (gd !== null) { return gd; } + var b = si.split(/\D/); + return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0); } } function parseISOLocal(s) { let si = s || ''; if (si === '') { return null }; - var b = s.split(/\D/); - return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); + var gd = parseGermanDate(si); + if (gd !== null) { return gd; } + var b = si.split(/\D/); + return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0); } function fnum(i, style) { /* { style: 'decimal/currency/percent', currency: 'USD/EUR', currencyDisplay: 'symbol/code/name', minimumIntegerDigits: 1, minimumFractionDigits: 2, maximumFractionDigits: 3, useGrouping: true } */ diff --git a/Fuchs/js/intranet/oci_main.js b/Fuchs/js/intranet/oci_main.js index 1650ac9..25c007a 100644 --- a/Fuchs/js/intranet/oci_main.js +++ b/Fuchs/js/intranet/oci_main.js @@ -11,9 +11,13 @@ $(this).remove(); api.rendered = false; }; - $ocms.isDateString = function (inp) { + /* Tests whether a string is a JSON/ISO-8601 date(-time) value as emitted by the + backend's JSON serializer (e.g. Newtonsoft "2021-09-09T00:00:00[.fff][Z|+hh:mm]"). + Deliberately NOT based on the native Date constructor, which guesses ambiguous + formats (e.g. dotted dd.MM.yy strings) heuristically and inconsistently. */ + $ocms.isJSONDateString = function (inp) { if (typeof inp !== 'string') { return false; } else { - return isNaN(new Date(inp)) === false; + return /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/.test(inp); } } $ocms.failure = function (jqXHR) { diff --git a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.eot b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.eot index b5aab37..b93a495 100644 Binary files a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.eot and b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.eot differ diff --git a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.ttf b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.ttf index 8dab117..1413fc6 100644 Binary files a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.ttf and b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.ttf differ diff --git a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff index a078bce..9e61285 100644 Binary files a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff and b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff differ diff --git a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff2 b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff2 index a631cf8..64539b5 100644 Binary files a/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff2 and b/Fuchs/wwwroot/fts/glyphicons-halflings-regular.woff2 differ diff --git a/Fuchs/wwwroot/web/fis.js b/Fuchs/wwwroot/web/fis.js index 7fe410b..85cd954 100644 --- a/Fuchs/wwwroot/web/fis.js +++ b/Fuchs/wwwroot/web/fis.js @@ -403,21 +403,34 @@ function ne(inp, alt) { return (inp || '') === '' ? (alt || '') : inp; } function pad(i, n) { return (i || '').toString().padStart(n, '0').substr(-1 * n); } +function twoDigitYear(y) { var n = parseInt(y, 10); return n + (n < 70 ? 2000 : 1900); } +/* Parses dot-separated German short dates (dd.MM.yyyy / dd.MM.yy, optionally with a time part). + Returns null if the string does not match, so callers can fall back to other parsing. */ +function parseGermanDate(si) { + var g = si.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2}|\d{4})(?:[\sT](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/); + if (g === null) { return null; } + var yr = g[3].length === 2 ? twoDigitYear(g[3]) : parseInt(g[3], 10); + return new Date(yr, parseInt(g[2], 10) - 1, parseInt(g[1], 10), parseInt(g[4] || '0', 10), parseInt(g[5] || '0', 10), parseInt(g[6] || '0', 10)); +} function parseISO(s) { let si = s || ''; if (si === '') { return null }; if (/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/.test(si) === true) { return new Date(si); } else { - var b = s.split(/\D/); - return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); + var gd = parseGermanDate(si); + if (gd !== null) { return gd; } + var b = si.split(/\D/); + return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0); } } function parseISOLocal(s) { let si = s || ''; if (si === '') { return null }; - var b = s.split(/\D/); - return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); + var gd = parseGermanDate(si); + if (gd !== null) { return gd; } + var b = si.split(/\D/); + return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0); } function fnum(i, style) { /* { style: 'decimal/currency/percent', currency: 'USD/EUR', currencyDisplay: 'symbol/code/name', minimumIntegerDigits: 1, minimumFractionDigits: 2, maximumFractionDigits: 3, useGrouping: true } */ @@ -1775,9 +1788,13 @@ class NumArray extends Array { $(this).remove(); api.rendered = false; }; - $ocms.isDateString = function (inp) { + /* Tests whether a string is a JSON/ISO-8601 date(-time) value as emitted by the + backend's JSON serializer (e.g. Newtonsoft "2021-09-09T00:00:00[.fff][Z|+hh:mm]"). + Deliberately NOT based on the native Date constructor, which guesses ambiguous + formats (e.g. dotted dd.MM.yy strings) heuristically and inconsistently. */ + $ocms.isJSONDateString = function (inp) { if (typeof inp !== 'string') { return false; } else { - return isNaN(new Date(inp)) === false; + return /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$/.test(inp); } } $ocms.failure = function (jqXHR) { @@ -2975,7 +2992,7 @@ $fis.resetPass = function (id, fds) { $fis.wdg = function (options) { let wf = $(this).empty(); $ocms.postXT({ - url: $ocms.url('wdg/one'), data: { short_name: options.wdg }, success: function (response, textStatus, jqXHR) { + url: $ocms.url('wdg/one'), data: { short_name: options.wdg }, timeout: 90000, success: function (response, textStatus, jqXHR) { let wi = options.wdg, wx = response[wi]; if (!wx) { wf.ldng(0); return; } let dbl = $.inArrayRegEx('dblwidth', wx.rendering_options) > -1, tiny = $.inArrayRegEx('tiny', wx.rendering_options) > -1; @@ -3000,7 +3017,7 @@ $fis.wdg = function (options) { var tdr = $$.tr().appendTo(tblset.bdy); $.each(wx.columns, function (ci, col) { var tdc = $$.td().appendTo(tdr); - if (dx[col] instanceof Date || $ocms.isDateString(dx[col]) === true) { + if (dx[col] instanceof Date || $ocms.isJSONDateString(dx[col]) === true) { tdc.text(fdt(dx[col], $t.dateformat)); } else { tdc.rwText(dx[col]); diff --git a/Fuchs/wwwroot/web/fis.min.js b/Fuchs/wwwroot/web/fis.min.js index 3387aa7..df2359d 100644 --- a/Fuchs/wwwroot/web/fis.min.js +++ b/Fuchs/wwwroot/web/fis.min.js @@ -1,4 +1,4 @@ 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."};$.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,r)=>{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 r=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()}(r===n)}))}else n(t)}!function(t){"use strict";var e=function(e,n,r,i){var o,a=t.document,s=a.createElement("link");if(n)o=n;else{var l=(a.body||a.getElementsByTagName("head")[0]).childNodes;o=l[l.length-1]}var c=a.styleSheets;if(i)for(var d in i)i.hasOwnProperty(d)&&s.setAttribute(d,i[d]);s.rel="stylesheet",s.href=e,s.media="only x",function t(e){if(a.body)return e();setTimeout((function(){t(e)}))}((function(){o.parentNode.insertBefore(s,n?o:o.nextSibling)}));var u=function(t){for(var e=s.href,n=c.length;n--;)if(c[n].href===e)return t();setTimeout((function(){u(t)}))};function f(){s.addEventListener&&s.removeEventListener("load",f),s.media=r||"all"}return s.addEventListener&&s.addEventListener("load",f),s.onloadcssdefined=u,u(f),s};"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,r;if("object"!=typeof t||null===t)return t;for(r in e=Array.isArray(t)?[]:{},t)n=t[r],e[r]=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 r=e,i=t.length>0&&e.split(";").some((function(e){for(var n,i=/[^yMdhms0-9]/gi,o=!0;null!==(n=i.exec(e));)o=o&&e.substr(n.index,1)===t.substr(n.index,1);var a=t.length===e.length&&o;return!0===a&&(r=e),a}));if(!0===i){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(r));)a["yMdhms".indexOf(o[0].substr(0,1))]=parseInt(("yy"===o[0]?"20":"")+t.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(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(),r=$("body");r.toggleClass("unfocus",n>vh()-1.2*t),r.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 r=e.internalText||t,i=e.internalCode||e.status;e.logtext=r+" ("+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 r={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,r,i){"false"===n||"not authorized"===n?("function"==typeof t.error&&t.error.apply(e,[i,r,n]),"function"==typeof $.status&&$.status(r+" - "+n)):"function"==typeof t.success&&t.success.apply(e,[n,r,i])},error:function(n,r,i){if($ocms.AjaxEX.call(n,r),-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"===r||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,r,i]):"function"==typeof $ocms.failure?$ocms.failure.apply(e,[n]):"function"==typeof $.status&&$.status("Server error: "+r+" - "+i)}},dataType:t.datatype||"json",complete:function(n,r){"function"==typeof t.complete&&t.complete.apply(e,[n,r]),$(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,r=Math.abs(e-t);n.setMilliseconds(n.getMilliseconds()+r),i.data({cex:n,ctt:r}),$ocms.cex_timer()}}},context:e,async:!0};"boolean"==typeof t.sync&&(r.async=!1===t.sync),!0==("boolean"==typeof t.contentType&&!1===t.contentType)&&(r.contentType=!1),$.ajax(r)}},$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"),r=new Date;if(e instanceof Date&&e.isValid()&&"number"==typeof n&&n>0&&e>r){let i=Math.abs(r-e)/n*100;t.css("width",i.toString()+"%"),i<98&&(!$ocms.cex_lp||Math.abs(r-$ocms.cex_lp)>6e5)&&$ocms.postXT({url:$ocms.url("ping"),success:()=>{$ocms.cex_lp=r},timeout:5e3,error:()=>{}})}},$ocms.vbl_send=function(t){var e=t.data||{};if(""!==(e.url||"")){var n=$("#contentframe form:first"),r={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,r){"function"==typeof e.error?e.error(r):"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(),o=$(this).prop("required")||!1;if(""!==e){var a=""!==n||!1===o;i=i&&a,!0===a?(r.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,r))}},function(t){t.fn.nza=function(e,n){var r=t(this).attr(e);return void 0!==r&&!1!==r?r:n||""},t.fn.serializeObject=function(e,n){var r=/\r?\n/g,i=/^(?:submit|button|image|reset|file)$/i,o=/^(?:input|select|textarea|keygen)/i,a=/^(?:checkbox|radio)$/i,s=bool((n=n||{}).typedvalues,!1),l={},c=t(this),d=c.find(':input:not([nosend],[type="file"])').addBack(":input"),u=!0;return t.each(d.not(".tinymce").get(),(function(n,c){var d=t(this),f=this,p=(this.type||"").toLowerCase(),m=d.prop("required")||!1;if(!0===(f.name&&!d.is(":disabled")&&o.test(f.nodeName)&&!i.test(p))){var h=d.val(),g=f.name,y=d.nza("data-format").split(":"),$=d.nza("pattern")||".*";if(!0===a.test(p)&&(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"===d.prop("type").substr(0,4)&&!1===isNaN(new Date(d.val()))&&(h=new Date(d.val())),h instanceof Date==!0&&"function"==typeof h.getMonth?!1===s&&(h=fdt(h,"date"===y[0]?"dts":"iso")):h=null;else if("number"===p&&!0===s){let t;t="integer"===y[0]?parseInt(h):parseFloat(h),h=isNaN(t)?h:t}if(!0!==m||""!==(h||"")&&null!==h.match($)?!0===bool(e,!1)&&f.setCustomValidity(""):(!0===bool(e,!1)&&f.setCustomValidity(d.nza("ocms-nvnote",$ocms.t.inv||"Invalid field")),h=null),null!=h&&"string"==typeof h){let t=l[g];null!=t?Array.isArray(t)?t.push(h.replace(r,"\r\n")):l[g]=[t,h.replace(r,"\r\n")]:l[g]=h.replace(r,"\r\n")}else if(null!=h){let t=l[g];null!=t?Array.isArray(t)?t.push(h):l[g]=[t,h]:l[g]=h}else u=!1}})),d.filter(".tinymce").each((function(e,n){var r=t(this),i=((this.type||"").toLowerCase(),r.prop("required")||!1);try{var o=tinymce.get(t(n).attr("id"));if(o){var a=t(n).attr("name"),s=o.getContent();!1===i||""!==(s||"")?l[a]=s:u=!1}}catch(e){t.noop()}})),c.toggleClass("invalid",!u),u?l:null},t.fn.sendForm=function(e,n,r){var i=t(this);r=r||{};var o={url:e,success:function(t){if(r.response=t,"function"==typeof n)n(t);i.closest("div.modal").remove()},error:function(t,e,n){"function"==typeof r.error?r.error.call(this,t):$ocms.failure.call(this,t)},complete:function(){i.ldng(0),"function"==typeof r.complete&&r.complete.call(this,jqXHR)}},a=i.find('input[type="file"]');o.data=new FormData,a.length>0&&t.each(a[0].files,(function(t,e){o.data.append(t,e),o.data.append("file_lastmodified",$ocms.isodt(e.lastModifiedDate))}));var s=i.serializeObject();t.each(s||{},(function(t,e){o.data.append(t,e)})),i.ldng(),$ocms.postXT.call(this,o)},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 r=t(this),i=$$.dc(e).attr(n||{}).insertAfter(r);return r.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),r=ne(e.find('[name="loginaccount"]').val(),$ocms.auth.account||$ocms.auth.requestedaccount||"");if(""!==r||!1===n){var i=e.find('[name="userlogin"]').empty().val(""),o=e.find('[name="username"]').empty().val(""),a=$("#dlg_userlogin_sel").empty().val(""),s=t.val()||"";if(!1===t.checkValidity()&&""===s)return;var l=t.closest("table").ldng();$ocms.postXT.call(this,{url:$ocms.url("auth"),data:{userinfo:s,account:r||""},success:function(t,e,n){if(1===t.length){var r=t[0];i.val(r.login).change().attr("required","").removeAttr("nosend"),o.val(r.name).change().attr("required","").show(),a.removeAttr("required").attr("nosend","").hide()}else t.length>0?(o.hide().removeAttr("required"),i.removeAttr("required").attr("nosend",""),0===a.length&&(a=$("").attr({name:"userlogin",size:t.length,id:"dlg_userlogin_sel",class:"form-control",required:""}).css({width:"100%","max-width":"100%",padding:"2px"}).insertAfter(o)),$.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(a);t%2==0&&n.css({"background-color":"#F9F9F9"})})),a.attr("required","").removeAttr("nosend")):(a.hide().attr("nosend",""),o.attr("required","").show(),i.attr("required","").removeAttr("nosend"),alert($t.l9))},error:function(t){$ocms.failure.call(this,t)},complete:function(){l.ldng(0)}})}else alert($t.l18)},sendpassword:function(t){var e=$(''),n=e.find(".form-body"),r=null;e.find("form").submit((function(t){t.preventDefault();var i=$(this).serializeObject(!0),o=null===r,a=o?"spwc":"spw";return $ocms.postXT.call(this,{url:$ocms.url(a),data:i,complete:function(){o?(n.append('
Ihnen wurde ein Code per SMS zugesandt.
Bitte tragen Sie den hier ein:
'),r=$('
').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,r){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 r?i.text(r):"object"==typeof r?i.attr(r):"function"==typeof r&&i.click(r),i},df:function(t){return $("
 
").attr(t||{})},opt:function(t,e,n){var r=$("");return"string"==typeof t?r.attr("value",t):"object"==typeof t&&r.attr(t),"string"==typeof e?r.text(e):"object"==typeof e&&r.attr(e),"object"==typeof n&&r.attr(n),r},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 r=n.closest("nav");r.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(r))}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,r=t||"";return $.each(e||[],(function(t,e){n=new RegExp("\\{"+t.toString()+"\\}","ig"),r=r.replace(n,e)})),r}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 r=t(this),i="function"==typeof n?n(r):n;return!0===("boolean"!=typeof i||i)&&r.appendTo(e),r},t.fn.appendIf=function(e,n){var r=t(this),i="function"==typeof n?n(r):n;return!0===("boolean"!=typeof i||i)&&r.append(e),r},t.fn.rwText=function(e,n,r){var i=t(this).empty();r=t.extend({wrap:!0},r);var o=!0===Array.isArray(e)?e:(null==e?"":String(e)).split("\n");return t.each(o,(function(t,e){""!==(e||"")&&(t>0&&i.append($$.br()),i.append(!0===r.wrap?$$.s(e):e))})),n&&i.attr("title",n),i},t.fn.loadSel=function(e,n,r){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 r&&r.call(i)}})}},t.fn.emptyWithEditors=function(e){var n=t(this);return n.find(":input.tinymce").each((function(e,n){try{var r=tinymce.get(t(n).attr("id"));r&&r.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,r){var i="regexp"===t.type(e)?e:new RegExp(e);if(!n)return-1;for(var o=r=r||0;o7){r=e.split(","),i=(n||(t<0?"rgb(0,0,0)":"rgb(255,255,255)")).split(",");var l=s(r[0].slice(4)),c=s(r[1]),d=s(r[2]);return"rgb("+(a((s(i[0].slice(4))-l)*o)+l)+","+(a((s(i[1])-c)*o)+c)+","+(a((s(i[2])-d)*o)+d)+")"}var u=(r=s(e.slice(1),16))>>16,f=r>>8&255,p=255&r;return"#"+(16777216+65536*(a((((i=s((n||(t<0?"#000000":"#FFFFFF")).slice(1),16))>>16)-u)*o)+u)+256*(a(((i>>8&255)-f)*o)+f)+(a(((255&i)-p)*o)+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 r=!0===("boolean"==typeof e&&e)&&"mouse",i="boolean"==typeof n&&n,o=t(this);return o.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:r,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:r,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},effect:!1})})),o},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 r=t(this);return $ocms.menu.call(r,e,n),r},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 r=(e[t]||"").toString().toUpperCase(),i=(n[t]||"").toString().toUpperCase();return console.debug(r.localeCompare(i)),r.localeCompare(i)})),this}sortNum(t){return this[0].sort(((e,n)=>{let r=e[t],i=n[t];return!0===isNaN(i)&&!1===isNaN(r)||ri?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 r=n[t];return e[r]||(e[r]=[]),e[r].push(n),e}),{})}each(t){if("function"==typeof t){let e=!1;this[0].forEach(((n,r,i)=>{if(!1===e){let o=t(n,r,i);"boolean"==typeof o&&!1===o&&(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.isDateString=function(t){return"string"==typeof t&&!1===isNaN(new Date(t))},t.failure=function(e){11110===(e.internalCode||-1)?t.login.dlg():alert($t.f1+"\n"+(e.internalText||""))},t.getScript=function(e,n){var r=[],i=[],o=function(t){return"string"==typeof t&&""!==(t||"")},a=function(t,e){!0===bool(e.condition,!0)&&(""!==(e.script||"")&&i.push({url:e.script,module:e.module||""}),!0===o(e.css||"")?r.push(e.css):!0===Array.isArray(e.css)&&Array.prototype.push.apply(r,e.css.filter(o)))};!0===o(e||"")?i.push(e):!0===Array.isArray(e)?$.each(e,a):"object"==typeof e&&""!==(e.script||"")&&a(0,e);let s=[];$.each(r,(function(t,e){""!==(e||"")&&s.push(loadCSS(e))}));let l=i.map((function(e,n){let r=e.url,o=e.module||"";if(""===o){return new Promise((function(t,e){try{!async function(){$.ajax({url:r,dataType:"script",success:function(){t(i)},error:function(){e(i)},timeout:3e4})}()}catch(t){console.debug(t.message+"%o",t)}}))}return t.loadmodule(o,r,e.alias)}));Promise.all(l).then(n)},t.loadmodule=function(e,n,r){return new Promise((function(i,o){!async function(){try{let a=(n.startsWith("/")||n.startsWith(".")?"":"/")+n;import(a).then((n=>{t[e]=n[r||"default"],i(e)})).catch((t=>{console.debug(t.message+"%o",t),o(e)}))}catch(t){console.debug(t.message+"%o",t)}}()}))},t.ocms_auth=function(e,n,r,i){!1===$.isPlainObject(t.auth.modules)&&(t.auth.modules={});var o=0;t.auth.modules[e+(r||"")]?((o=t.auth.modules[e+(r||"")])<2&&(r||"")===auth.guid&&(o=2),o>=(n||0)&&i(false)):t.postXT({url:t.url("auth"),data:{module:e,person_guid:r||""},success:function(a){o=a[e],t.auth.modules[e+(r||"")]=o,o<2&&(r||"")===t.auth.person_guid&&(o=2),o>=(n||0)&&i(false)},error:function(e){t.failure.call(this,e)}})},t.auth.locale="de",t.ocms_prepauth=function(e,n,r){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(){r()}})},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 r=$(this).removeClass("vis");if(!0===bool(n,!0)&&!1===r.is("#mainmenu")&&r.empty(),!1===bool(n,!1)&&r.is("#sidebar,#topbar")&&(e.unshift({id:"sbctrl",glyph:"glyphicon-th-list",aclass:"fbtn",fnc:function(){$lf()}}),$lf(0)),0===(e||[]).length)r.empty().addClass("hd");else{r.removeClass("hd");var i=!0===r.is("nav")?r:r.children("nav");1!==i.length&&(i=$("").tC("nv",r.is("#sidebar")).tC("ctxt",r.is("#topbar")).appendTo(r));var o,a=$$.ul().appendTo(i),s=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 r=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(n);$.each(e.itm||[],(function(t,e){o.call(r,e)}))},l=function(t){$(this).tC("disabled","boolean"==typeof t.disabled?t.disabled:"string"==typeof t.disabled&&"subs"===t.disabled&&0===(t.itm||[]).length)};o=function(e){var n,r=$$.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?r.attr("role",i).appendIf($$.s(e.lbl),""!==ne(e.lbl)):(n=$$.a({class:"on",role:"button"}).addClass(e.aclass).appendTo(r).append($$.s(e.lbl)),l.call(n,e),(e.itm||[]).length>0&&s.call(r,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 r,i=$$.li({id:n.id}).attr(n.attr||{}).addClass(n.lclass),s="string"==typeof n.fnc&&""!==n.fnc?n.fnc.split(":")[0]:"";if(""!==s&&"init"!==s)i.attr("role",s).appendIf($$.s(n.lbl),""!==ne(n.lbl));else{if(r=$$.a({class:"on",role:"button"}).addClass(n.aclass).appendTo(i),l.call(r,n),""!==(n.lbl||"")&&r.append($$.s(n.lbl)),""!==(n.ico||"")&&r.prepend($$.sc("ico "+n.ico)),""!==(n.glyph||"")&&r.prepend($$.sc("glyphicon "+n.glyph)),(n.itm||[]).length>0){i.addClass("dropdown"),r.append($$.sc("caret dd")).addClass("dds dropdown-toggle").attr({"aria-expanded":"false"});var c=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(i);$.each(n.itm||[],(function(t,e){o.call(c,e)}))}(n.sel||[]).length>0||(r.click($nuf),"function"==typeof n.fnc?r.click(n.data||{},n.fnc):"init"===s&&r.click($.extend({},n.data||{},{fn:n.fnc.split(":")[1]}),t.init))}i.appendTo(a)})),i.activatemenu()}},t.easytbl=(t,e)=>{e=e||{};let n=$$.tbl().addClass(e.class).css("border-collapse","collapse"),r=($$.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||r).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||r).rwText(n)))}return $.each(t||[],((t,i)=>{let o=$$.tr();$.each(i,((t,n)=>{n=n||"";let i=$$.td(o).css(e.cellcss||r);n instanceof jQuery?i.append(n):"string"==typeof n&&("<"===n.substring(0,1)?i.append(n):i.text(n))})),n.append(o)})),n},t.dlgtbl=(e,n,r)=>{r=r||{};let i=t.easytbl(e,r);t.dlg(i,$.extend({title:n},r))},t.dlg=function(t,n){n=n||{};let r=$("body > .modal").length>0,i=t=>typeof n[t],o=t=>"function"===i(t);if(!0===bool(n.exclusive,!0)&&!0===r)return void alert($t.dbldlg||"Es ist bereits ein Dialog geöffnet");let a=$$.dc("modal",$("body")),s=$$.dc("modal-dialog",a);!1===isNaN(n.zindex)?a.css("zIndex",n.zindex):!0===r&&a.css("zIndex",parseInt($("body > .modal:last").cssValue("zIndex"))+200),!1===isNaN(n.zindex_min)&&a.cssValue("zIndex")').appendTo(u)),""!==ne(n.title)&&(l=$$.dc("modal-header",u),$("

      ").text(n.title).appendTo(l));let p=$$.dc("modal-body",u),m=$$.dc("modal-footer",u);t instanceof jQuery==!0&&p.append(t);let h=function(t){t&&"function"==typeof t.stopPropagation&&t.stopPropagation(),s.removeClass("in"),!0===o("closing")&&n.closing.call(u),p.hide().emptyWithEditors(),a.remove(),!0===o("close")&&n.close.call(u)};if(u.find(":input[required]").length>0&&($$.dc("note_required",m).append($$.sc("ind_required","*")).append($$.s($t.t1||"Eingabe erforderlich")),$$.dc("note_invalid",m).append($$.s($t.t2||"Bitte überprüfen Sie Ihre Eingaben im Formular."))),!0===o("cancel")){$$.bbtn(n.cancelbutton||"Abbrechen","cancel").attr({type:"button",role:"cancel"}).appendTo(m).click((function(t){n.cancel.call(u,t);t.stopPropagation(),h()}))}if(!0===o("confirm")){let t=$$.bbtn(n.button||"OK","confirm").attr({type:!0===bool(n.form,!1)?"submit":"button",role:"confirm"}).appendTo(m);!0===f?(u.submit((function(t){try{n.confirm.call(u,t)}finally{t.preventDefault()}return!1})),u.on("modal_submit",(function(){n.confirm.call(u,e)}))):(t.click((function(t){n.confirm.call(u,t);t.stopPropagation()})),u.on("modal_submit",(function(){t.click()})))}else!0===f&&u.submit((function(t){return t.preventDefault(),!1}));return u.on("modal_close",(function(){h()})),c.click(h),!0===o("opening")&&n.opening.call(u),s.addClass("in"),ne(n.mode).indexOf("maxbody")>-1&&p.css("min-height",(d.height()-l.outerHeight()-m.outerHeight()).toString()+"px"),!0===o("open")&&n.open.call(u),{hd:l,bdy:p,ft:m,ct:d,dlg:s,c:u}},t.mform=function(e){let n=$$.dc("form-body"),r=Array.isArray(e)?e:e instanceof fields_definition?e.fields:[];return $.each(r||[],(function(e,r){let i=r.type||"";if("ignore"===i)return!0;let o=$$.dc("form-group",n),a=r.id||"dlg_"+(r.name||"")+("html"===r.type?"_"+(65536*(1+Math.random())||0).toString(16).substr(9):""),s=$$.lbl(r.label||r.name,{for:a}).appendTo($$.dc("form-itm",o)),l=$$.dc("form-itm",o),c=$$.i({id:a,name:r.name,placeholder:r.placeholder,type:r.type});switch(i){case"email":r.pattern=ne(r.pattern,"[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$");break;case"url":r.pattern=ne(r.pattern,"https?://.+");break;case"number":r.pattern=ne(r.pattern,"[-+]?[0-9]*[.,]?[0-9]*"),c.attr("step",r.precision||"any"),c.attr("data-format","float");break;case"integer":case"int":r.pattern=ne(r.pattern,"[-+]?[0-9]*"),c.attr("type","number"),c.attr("data-format","integer");break;case"date":if(""!==ne(r.pattern,$t.datepattern)&&(r.pattern=ne(r.pattern,"("+$t.datepattern+")|([0-9]{4}.(0[1-9]|1[012]).(0[1-9]|1[0-9]|2[0-9]|3[01]))")),""!==ne(r.placeholder,$t.dateplaceholder)&&c.attr("placeholder",ne(r.placeholder,$t.dateplaceholder)),"string"==typeof r.value){var d=r.value.substr(0,10);r.value="date"!==c.prop("type")?fdt(d+"T00:00:00",ne(r.dateformat,$t.dateformat)):d}c.attr("data-format","date:"+ne(r.dateformat,$t.dateformat)+";yyyy-MM-dd");break;case"datetime":c.attr("type","datetime-local"),""!==ne(r.pattern,$t.datetimepattern)&&(r.pattern=ne(r.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(r.placeholder,$t.datetimeplaceholder)&&c.attr("placeholder",ne(r.placeholder,$t.datetimeplaceholder)),"string"==typeof r.value&&"T"===r.value.substr(10,1)&&(r.value="datetime"!==c.prop("type").substr(0,8)?fdt(r.value,ne(r.datetimeformat,$t.datetimeformat)):r.value),c.attr("data-format","datetime:"+ne(r.datetimeformat,$t.datetimeformat)+";yyyy-MM-dd HH:mm:ss");break;case"hidden":o.addClass("hd");break;case"html":case"text":c=$$.txt({id:a,name:r.name,placeholder:r.placeholder,type:r.type}),c.tC("tinymce","html"===r.type);break;case"bool":case"boolean":r.url=[{value:"true",label:($t||{}).true||"Yes"},{value:"false",label:($t||{}).false||"No"}],"boolean"==typeof r.value&&(r.value=r.value?"true":"false");case"select":c=$$.sel({id:a,name:r.name,type:r.type}),!1===bool(r.required,!1)&&$$.eOpt().appendTo(c);try{var u=function(t){!0===Array.isArray(t)&&$.each(t,(function(t,e){"string"==typeof e?$$.opt(e,e).appendTo(c):!0===Array.isArray(e)?$$.opt(e[0],e[1]).appendTo(c):"object"==typeof e&&$$.opt(e.value,e.label||e.text).appendTo(c)}))};!0===Array.isArray(r.url)?u(r.url):"function"==typeof r.url?r.url.call(c):"string"==typeof r.url&&t.postXT({url:r.url,success:u})}catch(t){$.noop()}break;default:""!==ne(r["max-length"])&&c.attr("max-length",r["max-length"])}""!==ne(r.pattern)&&c.attr("pattern",r.pattern),c.val(r.value).change(),c.change((function(){$(this)[0].setCustomValidity("")})),c.addClass("form-control").prop("required",bool(r.required,!1)).prop("readonly",bool(r.readonly,!1)).appendTo(l),!0===bool(r.required,!1)&&s.append($$.sc("ind_required","*")),"object"==typeof r.attr&&c.attr(r.attr),"object"==typeof r.prop&&c.prop(r.prop),"string"==typeof r.class&&c.addClass(r.class),"function"==typeof r.change&&(c.change(r.change),!0===bool(r.applychange,!1)&&void 0!==r.value&&c.change()),""!==(r.note||"")&&$$.dc("form-note",l).rwText(r.note),"function"==typeof r.complete&&r.complete.call(c)})),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 r,i=$$.dc("frm").append(t.mform(e||[]).addClass("stacked"));n.addcontent instanceof jQuery&&i.append(n.addcontent),"function"==typeof n.submit?r=n.submit:"function"==typeof n.success&&(r=function(e){var r=$(this).ldng(1),i=$.extend({loginaccount:t.auth.account||""},r.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),r.trigger("modal_close")},error:function(){alert($t.l17)},complete:function(){r.ldng(0)},timeout:6e4}):(n.success.call(this,i),r.trigger("modal_close"))});let o={form:!0,title:n.title||"",button:n.button||$t.submit,confirm:r,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,o)},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 r=$$.dc("frm").append(t.mform(n).addClass("stacked")),i=t.dlg.call(this,r,{form:!0,title:$t.l0,button:$t.submit,confirm:function(n){var r=$(this).ldng(1),i=$.extend({loginaccount:t.auth.account||""},r.serializeObject());t.postXT({url:"/vt/login",data:i,success:function(n){""!==((n||{}).login||"")&&(r.trigger("modal_close"),t.auth=n,"object"==typeof e.ajo&&(e.ajo.islogin,$.ajax(e.ajo)))},error:function(){alert($t.l17)},complete:function(){r.ldng(0)},timeout:6e4})},size:[500,600]}),o=$$.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(o)},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,r;"object"==typeof window&&(window[t]=(n=function(t){var e=window,n=document.body,r=document.documentElement,i=Math.max(0,e.pageXOffset||r.scrollLeft||n.scrollLeft||0)-(r.clientLeft||0),o=Math.max(0,e.pageYOffset||r.scrollTop||n.scrollTop||0)-(r.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)-o:0}},(r=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:r,toArray:function(t){t=t||"id";for(var e=[],n="",r=0;rr.left&&er.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),r=this._container;!0===this._showDragItem&&this._moveItem(this._dragItem,e.x-this._click.x,e.y-this._click.y);for(var i=0;i0?s.mousedown(l).addClass("dctrl"):a.mousedown(l).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 r=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$ocms.wdg.call(r,{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"),r=e.children(".edit_frm");return n.length<1?n=$$.dc("cfrm hd").prependTo(e):!0===bool(t,!1)&&n.empty(),r.length<1&&(r=$$.dc("edit_frm").insertAfter(n)),r.empty()},frm_list:function(t,e){let n=$fis.cf(!1),r=n.children(".cfrm"),i=n.children(".list_frm");return r.length<1?r=$$.dc("cfrm hd").prependTo(n):!0===bool(t,!1)&&r.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,r)=>{$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:()=>{r()}})})),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},success:function(n,r,i){let o=t.wdg,a=n[o];if(!a)return void e.ldng(0);let s=$.inArrayRegEx("dblwidth",a.rendering_options)>-1,l=$.inArrayRegEx("tiny",a.rendering_options)>-1;e.toggleClass("dbl",s&&!l).toggleClass("tny",l);$$.dc("wdg_hd",e,{title:ne(a.description,$t.wdc)}).toggleClass("dbl",s).text(ne(a.name,t.wdg)).dblclick((function(t){t.stopPropagation(),$fis.wdg.call(e,{wdg:o})}));let c=$$.dc("wdg_cnt",e).toggleClass("dbl",s).hide(),d=$.inArrayRegEx("bgcolor",a.rendering_options);switch(d>-1&&c.css("backgroundColor",a.rendering_options[d].toString().right(":")),a.type){case"table":var u=$$.tblset({},c),f=$$.tr().appendTo(u.hd),p=$t.wdg[o.indexOf("wdg_ev_")>=0?"wdg_ev_":o]||{};$.each(a.columns,(function(t,e){var n=p[e]?p[e].label:e;$$.th().text(n).appendTo(f)})),$.each(a.data,(function(t,e){var n=$$.tr().appendTo(u.bdy);$.each(a.columns,(function(t,r){var i=$$.td().appendTo(n);e[r]instanceof Date||!0===$ocms.isDateString(e[r])?i.text(fdt(e[r],$t.dateformat)):i.rwText(e[r])}))})),$.inArray("firstrow_bold",a.rendering_options)>-1&&f.nextAll("tr:first").css("font-weight","bold");break;case"ind":$$.dc("ind",c).addClass("sts_"+(a.data.status||"")).append([$$.dc("ind").text(a.data.value),$$.lbl(a.data.label)]);break;case"image_url":c.css("background","url('"+a.url+"') no-repeat center center transparent");break;case"image_base64":c.css("background","url('data:image/png;base64,"+a.image+"') no-repeat center center transparent");break;case"html":if(c.html(a.html),$.inArray("reload_10min",a.rendering_options)>-1){var m=c.find("iframe");setTimeout((function(){m.attr("src",(function(t,e){return e}))}),6e5)}}$.inArray("reload_30min",a.rendering_options)>-1&&"html"!==a.type&&setTimeout((function(){$fis.wdg.call(e,{wdg:o})}),18e5),c.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 r=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$fis.wdg.call(r,{wdg:n})}))},loading:e})}};Array.prototype.push.apply($ocms.ocmsmenu,[{lbl:$t.m_inv,id:"m_inv",fnc:"init:inv",ico:"glyphicon glyphicon-list-alt"},{lbl:$t.m_req,id:"m_req",fnc:"init:req",ico:"glyphicon glyphicon-eur"},{lbl:$t.m_bcd,id:"m_bcd",fnc:"init:bam",ico:"glyphicon glyphicon-indent-right"},{fnc:"separator"},{lbl:$t.m_rep,id:"m_rep",fnc:"init:rep",ico:"glyphicon glyphicon-dashboard"},{fnc:"separator"},{lbl:$t.m_todo,id:"m_todo",fnc:()=>{$("#contentframe").empty().load($ocms.url("todos")),$("#listframe").rC("fix").aC("hd")},ico:"glyphicon glyphicon-sunglasses"}]),$(document).ready((function(){$fis.ov()})); \ No newline at end of file +function onloadCSS(t,e){e=e||{};let n=function(e){return new Promise(((n,r)=>{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 r=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()}(r===n)}))}else n(t)}!function(t){"use strict";var e=function(e,n,r,i){var o,a=t.document,s=a.createElement("link");if(n)o=n;else{var l=(a.body||a.getElementsByTagName("head")[0]).childNodes;o=l[l.length-1]}var c=a.styleSheets;if(i)for(var d in i)i.hasOwnProperty(d)&&s.setAttribute(d,i[d]);s.rel="stylesheet",s.href=e,s.media="only x",function t(e){if(a.body)return e();setTimeout((function(){t(e)}))}((function(){o.parentNode.insertBefore(s,n?o:o.nextSibling)}));var u=function(t){for(var e=s.href,n=c.length;n--;)if(c[n].href===e)return t();setTimeout((function(){u(t)}))};function p(){s.addEventListener&&s.removeEventListener("load",p),s.media=r||"all"}return s.addEventListener&&s.addEventListener("load",p),s.onloadcssdefined=u,u(p),s};"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,r;if("object"!=typeof t||null===t)return t;for(r in e=Array.isArray(t)?[]:{},t)n=t[r],e[r]=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 r=e,i=t.length>0&&e.split(";").some((function(e){for(var n,i=/[^yMdhms0-9]/gi,o=!0;null!==(n=i.exec(e));)o=o&&e.substr(n.index,1)===t.substr(n.index,1);var a=t.length===e.length&&o;return!0===a&&(r=e),a}));if(!0===i){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(r));)a["yMdhms".indexOf(o[0].substr(0,1))]=parseInt(("yy"===o[0]?"20":"")+t.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(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(),r=$("body");r.toggleClass("unfocus",n>vh()-1.2*t),r.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 r=e.internalText||t,i=e.internalCode||e.status;e.logtext=r+" ("+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 r={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,r,i){"false"===n||"not authorized"===n?("function"==typeof t.error&&t.error.apply(e,[i,r,n]),"function"==typeof $.status&&$.status(r+" - "+n)):"function"==typeof t.success&&t.success.apply(e,[n,r,i])},error:function(n,r,i){if($ocms.AjaxEX.call(n,r),-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"===r||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,r,i]):"function"==typeof $ocms.failure?$ocms.failure.apply(e,[n]):"function"==typeof $.status&&$.status("Server error: "+r+" - "+i)}},dataType:t.datatype||"json",complete:function(n,r){"function"==typeof t.complete&&t.complete.apply(e,[n,r]),$(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,r=Math.abs(e-t);n.setMilliseconds(n.getMilliseconds()+r),i.data({cex:n,ctt:r}),$ocms.cex_timer()}}},context:e,async:!0};"boolean"==typeof t.sync&&(r.async=!1===t.sync),!0==("boolean"==typeof t.contentType&&!1===t.contentType)&&(r.contentType=!1),$.ajax(r)}},$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"),r=new Date;if(e instanceof Date&&e.isValid()&&"number"==typeof n&&n>0&&e>r){let i=Math.abs(r-e)/n*100;t.css("width",i.toString()+"%"),i<98&&(!$ocms.cex_lp||Math.abs(r-$ocms.cex_lp)>6e5)&&$ocms.postXT({url:$ocms.url("ping"),success:()=>{$ocms.cex_lp=r},timeout:5e3,error:()=>{}})}},$ocms.vbl_send=function(t){var e=t.data||{};if(""!==(e.url||"")){var n=$("#contentframe form:first"),r={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,r){"function"==typeof e.error?e.error(r):"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(),o=$(this).prop("required")||!1;if(""!==e){var a=""!==n||!1===o;i=i&&a,!0===a?(r.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,r))}},function(t){t.fn.nza=function(e,n){var r=t(this).attr(e);return void 0!==r&&!1!==r?r:n||""},t.fn.serializeObject=function(e,n){var r=/\r?\n/g,i=/^(?:submit|button|image|reset|file)$/i,o=/^(?:input|select|textarea|keygen)/i,a=/^(?:checkbox|radio)$/i,s=bool((n=n||{}).typedvalues,!1),l={},c=t(this),d=c.find(':input:not([nosend],[type="file"])').addBack(":input"),u=!0;return t.each(d.not(".tinymce").get(),(function(n,c){var d=t(this),p=this,f=(this.type||"").toLowerCase(),m=d.prop("required")||!1;if(!0===(p.name&&!d.is(":disabled")&&o.test(p.nodeName)&&!i.test(f))){var h=d.val(),g=p.name,$=d.nza("data-format").split(":"),y=d.nza("pattern")||".*";if(!0===a.test(f)&&(h=p.checked?""!==h?h:"true":""),"date"===$[0].substr(0,4)&&$.length>1)"boolean"==typeof(h=parseDt(h,$.slice(1).join(":")))&&(h=null),null===h&&"date"===d.prop("type").substr(0,4)&&!1===isNaN(new Date(d.val()))&&(h=new Date(d.val())),h instanceof Date==!0&&"function"==typeof h.getMonth?!1===s&&(h=fdt(h,"date"===$[0]?"dts":"iso")):h=null;else if("number"===f&&!0===s){let t;t="integer"===$[0]?parseInt(h):parseFloat(h),h=isNaN(t)?h:t}if(!0!==m||""!==(h||"")&&null!==h.match(y)?!0===bool(e,!1)&&p.setCustomValidity(""):(!0===bool(e,!1)&&p.setCustomValidity(d.nza("ocms-nvnote",$ocms.t.inv||"Invalid field")),h=null),null!=h&&"string"==typeof h){let t=l[g];null!=t?Array.isArray(t)?t.push(h.replace(r,"\r\n")):l[g]=[t,h.replace(r,"\r\n")]:l[g]=h.replace(r,"\r\n")}else if(null!=h){let t=l[g];null!=t?Array.isArray(t)?t.push(h):l[g]=[t,h]:l[g]=h}else u=!1}})),d.filter(".tinymce").each((function(e,n){var r=t(this),i=((this.type||"").toLowerCase(),r.prop("required")||!1);try{var o=tinymce.get(t(n).attr("id"));if(o){var a=t(n).attr("name"),s=o.getContent();!1===i||""!==(s||"")?l[a]=s:u=!1}}catch(e){t.noop()}})),c.toggleClass("invalid",!u),u?l:null},t.fn.sendForm=function(e,n,r){var i=t(this);r=r||{};var o={url:e,success:function(t){if(r.response=t,"function"==typeof n)n(t);i.closest("div.modal").remove()},error:function(t,e,n){"function"==typeof r.error?r.error.call(this,t):$ocms.failure.call(this,t)},complete:function(){i.ldng(0),"function"==typeof r.complete&&r.complete.call(this,jqXHR)}},a=i.find('input[type="file"]');o.data=new FormData,a.length>0&&t.each(a[0].files,(function(t,e){o.data.append(t,e),o.data.append("file_lastmodified",$ocms.isodt(e.lastModifiedDate))}));var s=i.serializeObject();t.each(s||{},(function(t,e){o.data.append(t,e)})),i.ldng(),$ocms.postXT.call(this,o)},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 r=t(this),i=$$.dc(e).attr(n||{}).insertAfter(r);return r.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),r=ne(e.find('[name="loginaccount"]').val(),$ocms.auth.account||$ocms.auth.requestedaccount||"");if(""!==r||!1===n){var i=e.find('[name="userlogin"]').empty().val(""),o=e.find('[name="username"]').empty().val(""),a=$("#dlg_userlogin_sel").empty().val(""),s=t.val()||"";if(!1===t.checkValidity()&&""===s)return;var l=t.closest("table").ldng();$ocms.postXT.call(this,{url:$ocms.url("auth"),data:{userinfo:s,account:r||""},success:function(t,e,n){if(1===t.length){var r=t[0];i.val(r.login).change().attr("required","").removeAttr("nosend"),o.val(r.name).change().attr("required","").show(),a.removeAttr("required").attr("nosend","").hide()}else t.length>0?(o.hide().removeAttr("required"),i.removeAttr("required").attr("nosend",""),0===a.length&&(a=$("").attr({name:"userlogin",size:t.length,id:"dlg_userlogin_sel",class:"form-control",required:""}).css({width:"100%","max-width":"100%",padding:"2px"}).insertAfter(o)),$.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(a);t%2==0&&n.css({"background-color":"#F9F9F9"})})),a.attr("required","").removeAttr("nosend")):(a.hide().attr("nosend",""),o.attr("required","").show(),i.attr("required","").removeAttr("nosend"),alert($t.l9))},error:function(t){$ocms.failure.call(this,t)},complete:function(){l.ldng(0)}})}else alert($t.l18)},sendpassword:function(t){var e=$(''),n=e.find(".form-body"),r=null;e.find("form").submit((function(t){t.preventDefault();var i=$(this).serializeObject(!0),o=null===r,a=o?"spwc":"spw";return $ocms.postXT.call(this,{url:$ocms.url(a),data:i,complete:function(){o?(n.append('
      Ihnen wurde ein Code per SMS zugesandt.
      Bitte tragen Sie den hier ein:
      '),r=$('
      ').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,r){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 r?i.text(r):"object"==typeof r?i.attr(r):"function"==typeof r&&i.click(r),i},df:function(t){return $("
       
      ").attr(t||{})},opt:function(t,e,n){var r=$("");return"string"==typeof t?r.attr("value",t):"object"==typeof t&&r.attr(t),"string"==typeof e?r.text(e):"object"==typeof e&&r.attr(e),"object"==typeof n&&r.attr(n),r},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 r=n.closest("nav");r.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(r))}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,r=t||"";return $.each(e||[],(function(t,e){n=new RegExp("\\{"+t.toString()+"\\}","ig"),r=r.replace(n,e)})),r}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 r=t(this),i="function"==typeof n?n(r):n;return!0===("boolean"!=typeof i||i)&&r.appendTo(e),r},t.fn.appendIf=function(e,n){var r=t(this),i="function"==typeof n?n(r):n;return!0===("boolean"!=typeof i||i)&&r.append(e),r},t.fn.rwText=function(e,n,r){var i=t(this).empty();r=t.extend({wrap:!0},r);var o=!0===Array.isArray(e)?e:(null==e?"":String(e)).split("\n");return t.each(o,(function(t,e){""!==(e||"")&&(t>0&&i.append($$.br()),i.append(!0===r.wrap?$$.s(e):e))})),n&&i.attr("title",n),i},t.fn.loadSel=function(e,n,r){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 r&&r.call(i)}})}},t.fn.emptyWithEditors=function(e){var n=t(this);return n.find(":input.tinymce").each((function(e,n){try{var r=tinymce.get(t(n).attr("id"));r&&r.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,r){var i="regexp"===t.type(e)?e:new RegExp(e);if(!n)return-1;for(var o=r=r||0;o7){r=e.split(","),i=(n||(t<0?"rgb(0,0,0)":"rgb(255,255,255)")).split(",");var l=s(r[0].slice(4)),c=s(r[1]),d=s(r[2]);return"rgb("+(a((s(i[0].slice(4))-l)*o)+l)+","+(a((s(i[1])-c)*o)+c)+","+(a((s(i[2])-d)*o)+d)+")"}var u=(r=s(e.slice(1),16))>>16,p=r>>8&255,f=255&r;return"#"+(16777216+65536*(a((((i=s((n||(t<0?"#000000":"#FFFFFF")).slice(1),16))>>16)-u)*o)+u)+256*(a(((i>>8&255)-p)*o)+p)+(a(((255&i)-f)*o)+f)).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 r=!0===("boolean"==typeof e&&e)&&"mouse",i="boolean"==typeof n&&n,o=t(this);return o.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:r,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:r,adjust:{x:2,y:2},viewport:!0},events:{hidden:$ocms.tooltip_hidden},effect:!1})})),o},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 r=t(this);return $ocms.menu.call(r,e,n),r},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 r=(e[t]||"").toString().toUpperCase(),i=(n[t]||"").toString().toUpperCase();return console.debug(r.localeCompare(i)),r.localeCompare(i)})),this}sortNum(t){return this[0].sort(((e,n)=>{let r=e[t],i=n[t];return!0===isNaN(i)&&!1===isNaN(r)||ri?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 r=n[t];return e[r]||(e[r]=[]),e[r].push(n),e}),{})}each(t){if("function"==typeof t){let e=!1;this[0].forEach(((n,r,i)=>{if(!1===e){let o=t(n,r,i);"boolean"==typeof o&&!1===o&&(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 r=[],i=[],o=function(t){return"string"==typeof t&&""!==(t||"")},a=function(t,e){!0===bool(e.condition,!0)&&(""!==(e.script||"")&&i.push({url:e.script,module:e.module||""}),!0===o(e.css||"")?r.push(e.css):!0===Array.isArray(e.css)&&Array.prototype.push.apply(r,e.css.filter(o)))};!0===o(e||"")?i.push(e):!0===Array.isArray(e)?$.each(e,a):"object"==typeof e&&""!==(e.script||"")&&a(0,e);let s=[];$.each(r,(function(t,e){""!==(e||"")&&s.push(loadCSS(e))}));let l=i.map((function(e,n){let r=e.url,o=e.module||"";if(""===o){return new Promise((function(t,e){try{!async function(){$.ajax({url:r,dataType:"script",success:function(){t(i)},error:function(){e(i)},timeout:3e4})}()}catch(t){console.debug(t.message+"%o",t)}}))}return t.loadmodule(o,r,e.alias)}));Promise.all(l).then(n)},t.loadmodule=function(e,n,r){return new Promise((function(i,o){!async function(){try{let a=(n.startsWith("/")||n.startsWith(".")?"":"/")+n;import(a).then((n=>{t[e]=n[r||"default"],i(e)})).catch((t=>{console.debug(t.message+"%o",t),o(e)}))}catch(t){console.debug(t.message+"%o",t)}}()}))},t.ocms_auth=function(e,n,r,i){!1===$.isPlainObject(t.auth.modules)&&(t.auth.modules={});var o=0;t.auth.modules[e+(r||"")]?((o=t.auth.modules[e+(r||"")])<2&&(r||"")===auth.guid&&(o=2),o>=(n||0)&&i(false)):t.postXT({url:t.url("auth"),data:{module:e,person_guid:r||""},success:function(a){o=a[e],t.auth.modules[e+(r||"")]=o,o<2&&(r||"")===t.auth.person_guid&&(o=2),o>=(n||0)&&i(false)},error:function(e){t.failure.call(this,e)}})},t.auth.locale="de",t.ocms_prepauth=function(e,n,r){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(){r()}})},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 r=$(this).removeClass("vis");if(!0===bool(n,!0)&&!1===r.is("#mainmenu")&&r.empty(),!1===bool(n,!1)&&r.is("#sidebar,#topbar")&&(e.unshift({id:"sbctrl",glyph:"glyphicon-th-list",aclass:"fbtn",fnc:function(){$lf()}}),$lf(0)),0===(e||[]).length)r.empty().addClass("hd");else{r.removeClass("hd");var i=!0===r.is("nav")?r:r.children("nav");1!==i.length&&(i=$("").tC("nv",r.is("#sidebar")).tC("ctxt",r.is("#topbar")).appendTo(r));var o,a=$$.ul().appendTo(i),s=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 r=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(n);$.each(e.itm||[],(function(t,e){o.call(r,e)}))},l=function(t){$(this).tC("disabled","boolean"==typeof t.disabled?t.disabled:"string"==typeof t.disabled&&"subs"===t.disabled&&0===(t.itm||[]).length)};o=function(e){var n,r=$$.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?r.attr("role",i).appendIf($$.s(e.lbl),""!==ne(e.lbl)):(n=$$.a({class:"on",role:"button"}).addClass(e.aclass).appendTo(r).append($$.s(e.lbl)),l.call(n,e),(e.itm||[]).length>0&&s.call(r,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 r,i=$$.li({id:n.id}).attr(n.attr||{}).addClass(n.lclass),s="string"==typeof n.fnc&&""!==n.fnc?n.fnc.split(":")[0]:"";if(""!==s&&"init"!==s)i.attr("role",s).appendIf($$.s(n.lbl),""!==ne(n.lbl));else{if(r=$$.a({class:"on",role:"button"}).addClass(n.aclass).appendTo(i),l.call(r,n),""!==(n.lbl||"")&&r.append($$.s(n.lbl)),""!==(n.ico||"")&&r.prepend($$.sc("ico "+n.ico)),""!==(n.glyph||"")&&r.prepend($$.sc("glyphicon "+n.glyph)),(n.itm||[]).length>0){i.addClass("dropdown"),r.append($$.sc("caret dd")).addClass("dds dropdown-toggle").attr({"aria-expanded":"false"});var c=$$.ul({class:"dropdown-menu",role:"menu"}).appendTo(i);$.each(n.itm||[],(function(t,e){o.call(c,e)}))}(n.sel||[]).length>0||(r.click($nuf),"function"==typeof n.fnc?r.click(n.data||{},n.fnc):"init"===s&&r.click($.extend({},n.data||{},{fn:n.fnc.split(":")[1]}),t.init))}i.appendTo(a)})),i.activatemenu()}},t.easytbl=(t,e)=>{e=e||{};let n=$$.tbl().addClass(e.class).css("border-collapse","collapse"),r=($$.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||r).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||r).rwText(n)))}return $.each(t||[],((t,i)=>{let o=$$.tr();$.each(i,((t,n)=>{n=n||"";let i=$$.td(o).css(e.cellcss||r);n instanceof jQuery?i.append(n):"string"==typeof n&&("<"===n.substring(0,1)?i.append(n):i.text(n))})),n.append(o)})),n},t.dlgtbl=(e,n,r)=>{r=r||{};let i=t.easytbl(e,r);t.dlg(i,$.extend({title:n},r))},t.dlg=function(t,n){n=n||{};let r=$("body > .modal").length>0,i=t=>typeof n[t],o=t=>"function"===i(t);if(!0===bool(n.exclusive,!0)&&!0===r)return void alert($t.dbldlg||"Es ist bereits ein Dialog geöffnet");let a=$$.dc("modal",$("body")),s=$$.dc("modal-dialog",a);!1===isNaN(n.zindex)?a.css("zIndex",n.zindex):!0===r&&a.css("zIndex",parseInt($("body > .modal:last").cssValue("zIndex"))+200),!1===isNaN(n.zindex_min)&&a.cssValue("zIndex")').appendTo(u)),""!==ne(n.title)&&(l=$$.dc("modal-header",u),$("

        ").text(n.title).appendTo(l));let f=$$.dc("modal-body",u),m=$$.dc("modal-footer",u);t instanceof jQuery==!0&&f.append(t);let h=function(t){t&&"function"==typeof t.stopPropagation&&t.stopPropagation(),s.removeClass("in"),!0===o("closing")&&n.closing.call(u),f.hide().emptyWithEditors(),a.remove(),!0===o("close")&&n.close.call(u)};if(u.find(":input[required]").length>0&&($$.dc("note_required",m).append($$.sc("ind_required","*")).append($$.s($t.t1||"Eingabe erforderlich")),$$.dc("note_invalid",m).append($$.s($t.t2||"Bitte überprüfen Sie Ihre Eingaben im Formular."))),!0===o("cancel")){$$.bbtn(n.cancelbutton||"Abbrechen","cancel").attr({type:"button",role:"cancel"}).appendTo(m).click((function(t){n.cancel.call(u,t);t.stopPropagation(),h()}))}if(!0===o("confirm")){let t=$$.bbtn(n.button||"OK","confirm").attr({type:!0===bool(n.form,!1)?"submit":"button",role:"confirm"}).appendTo(m);!0===p?(u.submit((function(t){try{n.confirm.call(u,t)}finally{t.preventDefault()}return!1})),u.on("modal_submit",(function(){n.confirm.call(u,e)}))):(t.click((function(t){n.confirm.call(u,t);t.stopPropagation()})),u.on("modal_submit",(function(){t.click()})))}else!0===p&&u.submit((function(t){return t.preventDefault(),!1}));return u.on("modal_close",(function(){h()})),c.click(h),!0===o("opening")&&n.opening.call(u),s.addClass("in"),ne(n.mode).indexOf("maxbody")>-1&&f.css("min-height",(d.height()-l.outerHeight()-m.outerHeight()).toString()+"px"),!0===o("open")&&n.open.call(u),{hd:l,bdy:f,ft:m,ct:d,dlg:s,c:u}},t.mform=function(e){let n=$$.dc("form-body"),r=Array.isArray(e)?e:e instanceof fields_definition?e.fields:[];return $.each(r||[],(function(e,r){let i=r.type||"";if("ignore"===i)return!0;let o=$$.dc("form-group",n),a=r.id||"dlg_"+(r.name||"")+("html"===r.type?"_"+(65536*(1+Math.random())||0).toString(16).substr(9):""),s=$$.lbl(r.label||r.name,{for:a}).appendTo($$.dc("form-itm",o)),l=$$.dc("form-itm",o),c=$$.i({id:a,name:r.name,placeholder:r.placeholder,type:r.type});switch(i){case"email":r.pattern=ne(r.pattern,"[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$");break;case"url":r.pattern=ne(r.pattern,"https?://.+");break;case"number":r.pattern=ne(r.pattern,"[-+]?[0-9]*[.,]?[0-9]*"),c.attr("step",r.precision||"any"),c.attr("data-format","float");break;case"integer":case"int":r.pattern=ne(r.pattern,"[-+]?[0-9]*"),c.attr("type","number"),c.attr("data-format","integer");break;case"date":if(""!==ne(r.pattern,$t.datepattern)&&(r.pattern=ne(r.pattern,"("+$t.datepattern+")|([0-9]{4}.(0[1-9]|1[012]).(0[1-9]|1[0-9]|2[0-9]|3[01]))")),""!==ne(r.placeholder,$t.dateplaceholder)&&c.attr("placeholder",ne(r.placeholder,$t.dateplaceholder)),"string"==typeof r.value){var d=r.value.substr(0,10);r.value="date"!==c.prop("type")?fdt(d+"T00:00:00",ne(r.dateformat,$t.dateformat)):d}c.attr("data-format","date:"+ne(r.dateformat,$t.dateformat)+";yyyy-MM-dd");break;case"datetime":c.attr("type","datetime-local"),""!==ne(r.pattern,$t.datetimepattern)&&(r.pattern=ne(r.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(r.placeholder,$t.datetimeplaceholder)&&c.attr("placeholder",ne(r.placeholder,$t.datetimeplaceholder)),"string"==typeof r.value&&"T"===r.value.substr(10,1)&&(r.value="datetime"!==c.prop("type").substr(0,8)?fdt(r.value,ne(r.datetimeformat,$t.datetimeformat)):r.value),c.attr("data-format","datetime:"+ne(r.datetimeformat,$t.datetimeformat)+";yyyy-MM-dd HH:mm:ss");break;case"hidden":o.addClass("hd");break;case"html":case"text":c=$$.txt({id:a,name:r.name,placeholder:r.placeholder,type:r.type}),c.tC("tinymce","html"===r.type);break;case"bool":case"boolean":r.url=[{value:"true",label:($t||{}).true||"Yes"},{value:"false",label:($t||{}).false||"No"}],"boolean"==typeof r.value&&(r.value=r.value?"true":"false");case"select":c=$$.sel({id:a,name:r.name,type:r.type}),!1===bool(r.required,!1)&&$$.eOpt().appendTo(c);try{var u=function(t){!0===Array.isArray(t)&&$.each(t,(function(t,e){"string"==typeof e?$$.opt(e,e).appendTo(c):!0===Array.isArray(e)?$$.opt(e[0],e[1]).appendTo(c):"object"==typeof e&&$$.opt(e.value,e.label||e.text).appendTo(c)}))};!0===Array.isArray(r.url)?u(r.url):"function"==typeof r.url?r.url.call(c):"string"==typeof r.url&&t.postXT({url:r.url,success:u})}catch(t){$.noop()}break;default:""!==ne(r["max-length"])&&c.attr("max-length",r["max-length"])}""!==ne(r.pattern)&&c.attr("pattern",r.pattern),c.val(r.value).change(),c.change((function(){$(this)[0].setCustomValidity("")})),c.addClass("form-control").prop("required",bool(r.required,!1)).prop("readonly",bool(r.readonly,!1)).appendTo(l),!0===bool(r.required,!1)&&s.append($$.sc("ind_required","*")),"object"==typeof r.attr&&c.attr(r.attr),"object"==typeof r.prop&&c.prop(r.prop),"string"==typeof r.class&&c.addClass(r.class),"function"==typeof r.change&&(c.change(r.change),!0===bool(r.applychange,!1)&&void 0!==r.value&&c.change()),""!==(r.note||"")&&$$.dc("form-note",l).rwText(r.note),"function"==typeof r.complete&&r.complete.call(c)})),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 r,i=$$.dc("frm").append(t.mform(e||[]).addClass("stacked"));n.addcontent instanceof jQuery&&i.append(n.addcontent),"function"==typeof n.submit?r=n.submit:"function"==typeof n.success&&(r=function(e){var r=$(this).ldng(1),i=$.extend({loginaccount:t.auth.account||""},r.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),r.trigger("modal_close")},error:function(){alert($t.l17)},complete:function(){r.ldng(0)},timeout:6e4}):(n.success.call(this,i),r.trigger("modal_close"))});let o={form:!0,title:n.title||"",button:n.button||$t.submit,confirm:r,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,o)},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 r=$$.dc("frm").append(t.mform(n).addClass("stacked")),i=t.dlg.call(this,r,{form:!0,title:$t.l0,button:$t.submit,confirm:function(n){var r=$(this).ldng(1),i=$.extend({loginaccount:t.auth.account||""},r.serializeObject());t.postXT({url:"/vt/login",data:i,success:function(n){""!==((n||{}).login||"")&&(r.trigger("modal_close"),t.auth=n,"object"==typeof e.ajo&&(e.ajo.islogin,$.ajax(e.ajo)))},error:function(){alert($t.l17)},complete:function(){r.ldng(0)},timeout:6e4})},size:[500,600]}),o=$$.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(o)},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,r;"object"==typeof window&&(window[t]=(n=function(t){var e=window,n=document.body,r=document.documentElement,i=Math.max(0,e.pageXOffset||r.scrollLeft||n.scrollLeft||0)-(r.clientLeft||0),o=Math.max(0,e.pageYOffset||r.scrollTop||n.scrollTop||0)-(r.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)-o:0}},(r=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:r,toArray:function(t){t=t||"id";for(var e=[],n="",r=0;rr.left&&er.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),r=this._container;!0===this._showDragItem&&this._moveItem(this._dragItem,e.x-this._click.x,e.y-this._click.y);for(var i=0;i0?s.mousedown(l).addClass("dctrl"):a.mousedown(l).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 r=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$ocms.wdg.call(r,{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"),r=e.children(".edit_frm");return n.length<1?n=$$.dc("cfrm hd").prependTo(e):!0===bool(t,!1)&&n.empty(),r.length<1&&(r=$$.dc("edit_frm").insertAfter(n)),r.empty()},frm_list:function(t,e){let n=$fis.cf(!1),r=n.children(".cfrm"),i=n.children(".list_frm");return r.length<1?r=$$.dc("cfrm hd").prependTo(n):!0===bool(t,!1)&&r.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,r)=>{$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:()=>{r()}})})),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,r,i){let o=t.wdg,a=n[o];if(!a)return void e.ldng(0);let s=$.inArrayRegEx("dblwidth",a.rendering_options)>-1,l=$.inArrayRegEx("tiny",a.rendering_options)>-1;e.toggleClass("dbl",s&&!l).toggleClass("tny",l);$$.dc("wdg_hd",e,{title:ne(a.description,$t.wdc)}).toggleClass("dbl",s).text(ne(a.name,t.wdg)).dblclick((function(t){t.stopPropagation(),$fis.wdg.call(e,{wdg:o})}));let c=$$.dc("wdg_cnt",e).toggleClass("dbl",s).hide(),d=$.inArrayRegEx("bgcolor",a.rendering_options);switch(d>-1&&c.css("backgroundColor",a.rendering_options[d].toString().right(":")),a.type){case"table":var u=$$.tblset({},c),p=$$.tr().appendTo(u.hd),f=$t.wdg[o.indexOf("wdg_ev_")>=0?"wdg_ev_":o]||{};$.each(a.columns,(function(t,e){var n=f[e]?f[e].label:e;$$.th().text(n).appendTo(p)})),$.each(a.data,(function(t,e){var n=$$.tr().appendTo(u.bdy);$.each(a.columns,(function(t,r){var i=$$.td().appendTo(n);e[r]instanceof Date||!0===$ocms.isJSONDateString(e[r])?i.text(fdt(e[r],$t.dateformat)):i.rwText(e[r])}))})),$.inArray("firstrow_bold",a.rendering_options)>-1&&p.nextAll("tr:first").css("font-weight","bold");break;case"ind":$$.dc("ind",c).addClass("sts_"+(a.data.status||"")).append([$$.dc("ind").text(a.data.value),$$.lbl(a.data.label)]);break;case"image_url":c.css("background","url('"+a.url+"') no-repeat center center transparent");break;case"image_base64":c.css("background","url('data:image/png;base64,"+a.image+"') no-repeat center center transparent");break;case"html":if(c.html(a.html),$.inArray("reload_10min",a.rendering_options)>-1){var m=c.find("iframe");setTimeout((function(){m.attr("src",(function(t,e){return e}))}),6e5)}}$.inArray("reload_30min",a.rendering_options)>-1&&"html"!==a.type&&setTimeout((function(){$fis.wdg.call(e,{wdg:o})}),18e5),c.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 r=$$.dc("wdg_frame",e,{"data-wdg":n}).ldng(1);$fis.wdg.call(r,{wdg:n})}))},loading:e})}};Array.prototype.push.apply($ocms.ocmsmenu,[{lbl:$t.m_inv,id:"m_inv",fnc:"init:inv",ico:"glyphicon glyphicon-list-alt"},{lbl:$t.m_req,id:"m_req",fnc:"init:req",ico:"glyphicon glyphicon-eur"},{lbl:$t.m_bcd,id:"m_bcd",fnc:"init:bam",ico:"glyphicon glyphicon-indent-right"},{fnc:"separator"},{lbl:$t.m_rep,id:"m_rep",fnc:"init:rep",ico:"glyphicon glyphicon-dashboard"},{fnc:"separator"},{lbl:$t.m_todo,id:"m_todo",fnc:()=>{$("#contentframe").empty().load($ocms.url("todos")),$("#listframe").rC("fix").aC("hd")},ico:"glyphicon glyphicon-sunglasses"}]),$(document).ready((function(){$fis.ov()})); \ No newline at end of file diff --git a/Fuchs/wwwroot/web/fisb.js b/Fuchs/wwwroot/web/fisb.js index aee91b3..8eb9fc4 100644 --- a/Fuchs/wwwroot/web/fisb.js +++ b/Fuchs/wwwroot/web/fisb.js @@ -222,21 +222,34 @@ function ne(inp, alt) { return (inp || '') === '' ? (alt || '') : inp; } function pad(i, n) { return (i || '').toString().padStart(n, '0').substr(-1 * n); } +function twoDigitYear(y) { var n = parseInt(y, 10); return n + (n < 70 ? 2000 : 1900); } +/* Parses dot-separated German short dates (dd.MM.yyyy / dd.MM.yy, optionally with a time part). + Returns null if the string does not match, so callers can fall back to other parsing. */ +function parseGermanDate(si) { + var g = si.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2}|\d{4})(?:[\sT](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/); + if (g === null) { return null; } + var yr = g[3].length === 2 ? twoDigitYear(g[3]) : parseInt(g[3], 10); + return new Date(yr, parseInt(g[2], 10) - 1, parseInt(g[1], 10), parseInt(g[4] || '0', 10), parseInt(g[5] || '0', 10), parseInt(g[6] || '0', 10)); +} function parseISO(s) { let si = s || ''; if (si === '') { return null }; if (/\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/.test(si) === true) { return new Date(si); } else { - var b = s.split(/\D/); - return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); + var gd = parseGermanDate(si); + if (gd !== null) { return gd; } + var b = si.split(/\D/); + return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0); } } function parseISOLocal(s) { let si = s || ''; if (si === '') { return null }; - var b = s.split(/\D/); - return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]); + var gd = parseGermanDate(si); + if (gd !== null) { return gd; } + var b = si.split(/\D/); + return new Date(b[0], b[1] - 1, b[2], b[3] || 0, b[4] || 0, b[5] || 0); } function fnum(i, style) { /* { style: 'decimal/currency/percent', currency: 'USD/EUR', currencyDisplay: 'symbol/code/name', minimumIntegerDigits: 1, minimumFractionDigits: 2, maximumFractionDigits: 3, useGrouping: true } */ diff --git a/Fuchs/wwwroot/web/fisb.min.js b/Fuchs/wwwroot/web/fisb.min.js index 1a9f864..f10f9f6 100644 --- a/Fuchs/wwwroot/web/fisb.min.js +++ b/Fuchs/wwwroot/web/fisb.min.js @@ -1 +1 @@ -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};"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),d=c.find(':input:not([nosend],[type="file"])').addBack(":input"),u=!0;return e.each(d.not(".tinymce").get(),(function(n,c){var d=e(this),f=this,m=(this.type||"").toLowerCase(),p=d.prop("required")||!1;if(!0===(f.name&&!d.is(":disabled")&&o.test(f.nodeName)&&!r.test(m))){var h=d.val(),g=f.name,y=d.nza("data-format").split(":"),v=d.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"===d.prop("type").substr(0,4)&&!1===isNaN(new Date(d.val()))&&(h=new Date(d.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&&!0===s){let e;e="integer"===y[0]?parseInt(h):parseFloat(h),h=isNaN(e)?h:e}if(!0!==p||""!==(h||"")&&null!==h.match(v)?!0===bool(t,!1)&&f.setCustomValidity(""):(!0===bool(t,!1)&&f.setCustomValidity(d.nza("ocms-nvnote",$ocms.t.inv||"Invalid field")),h=null),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 u=!1}})),d.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:u=!1}}catch(t){e.noop()}})),c.toggleClass("invalid",!u),u?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 +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};"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&&!0===s){let e;e="integer"===y[0]?parseInt(h):parseFloat(h),h=isNaN(e)?h:e}if(!0!==p||""!==(h||"")&&null!==h.match(v)?!0===bool(t,!1)&&f.setCustomValidity(""):(!0===bool(t,!1)&&f.setCustomValidity(u.nza("ocms-nvnote",$ocms.t.inv||"Invalid field")),h=null),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/Fuchs_DataService.csproj b/Fuchs_DataService/Fuchs_DataService.csproj index 14d81cb..530a636 100644 --- a/Fuchs_DataService/Fuchs_DataService.csproj +++ b/Fuchs_DataService/Fuchs_DataService.csproj @@ -37,9 +37,9 @@ - - - - + + + +
        diff --git a/Fuchs_Database/FuchsDatabase.sqlproj b/Fuchs_Database/FuchsDatabase.sqlproj index b9fbe26..cd90244 100644 --- a/Fuchs_Database/FuchsDatabase.sqlproj +++ b/Fuchs_Database/FuchsDatabase.sqlproj @@ -1,4 +1,4 @@ - + Debug @@ -329,6 +329,8 @@ + + @@ -336,6 +338,8 @@ + + diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getInvoiceFileContent.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getInvoiceFileContent.sql new file mode 100644 index 0000000..301015e --- /dev/null +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getInvoiceFileContent.sql @@ -0,0 +1,18 @@ + +-- ============================================= +-- Author: +-- Create date: +-- Description: Returns the stored file bytes for a single invoice, for use by +-- the Azure Blob Storage startup backfill after the row has been +-- selected via fds__getInvoiceFiles_ForBlobArchive. +-- ============================================= +CREATE PROCEDURE [dbo].[fds__getInvoiceFileContent] + @Id varchar(10) +AS +BEGIN + SET NOCOUNT ON; + + SELECT TOP(1) [Id], [file] + FROM [dbo].[fds__invoices] + WHERE [Id] = @Id AND @Id is not null AND [file] IS NOT NULL; +END diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getInvoiceFiles_ForBlobArchive.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getInvoiceFiles_ForBlobArchive.sql new file mode 100644 index 0000000..8dd0ed8 --- /dev/null +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getInvoiceFiles_ForBlobArchive.sql @@ -0,0 +1,27 @@ + +-- ============================================= +-- Author: +-- Create date: +-- Description: Enumerates invoices that already have a stored file, returning +-- lightweight metadata only (no file bytes) so the Azure Blob +-- Storage startup backfill (see AzureBlobStorageService / +-- DocumentArchiveSyncService) can enumerate candidates cheaply +-- before fetching each file's content individually via +-- fds__getInvoiceFileContent. +-- ============================================= +CREATE PROCEDURE [dbo].[fds__getInvoiceFiles_ForBlobArchive] +AS +BEGIN + SET NOCOUNT ON; + + SELECT + [Id] + ,[Version] + ,[InvoiceId] + ,[InvoiceTitle] + ,[DocumentName] + ,[file_guid] + FROM [dbo].[fds__invoices] + WHERE [file] IS NOT NULL + ORDER BY [Id]; +END diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getReminderFileContent.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getReminderFileContent.sql new file mode 100644 index 0000000..a7f817b --- /dev/null +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getReminderFileContent.sql @@ -0,0 +1,18 @@ + +-- ============================================= +-- Author: +-- Create date: +-- Description: Returns the stored file bytes for a single reminder, for use by +-- the Azure Blob Storage startup backfill after the row has been +-- selected via fds__getReminderFiles_ForBlobArchive. +-- ============================================= +CREATE PROCEDURE [dbo].[fds__getReminderFileContent] + @Id varchar(10) +AS +BEGIN + SET NOCOUNT ON; + + SELECT TOP(1) [Id], [file] + FROM [dbo].[fds__reminder] + WHERE [Id] = @Id AND @Id is not null AND [file] IS NOT NULL; +END diff --git a/Fuchs_Database/dbo/Stored Procedures/fds__getReminderFiles_ForBlobArchive.sql b/Fuchs_Database/dbo/Stored Procedures/fds__getReminderFiles_ForBlobArchive.sql new file mode 100644 index 0000000..c438e84 --- /dev/null +++ b/Fuchs_Database/dbo/Stored Procedures/fds__getReminderFiles_ForBlobArchive.sql @@ -0,0 +1,25 @@ + +-- ============================================= +-- Author: +-- Create date: +-- Description: Enumerates reminders that already have a stored file, returning +-- lightweight metadata only (no file bytes) so the Azure Blob +-- Storage startup backfill (see AzureBlobStorageService / +-- DocumentArchiveSyncService) can enumerate candidates cheaply +-- before fetching each file's content individually via +-- fds__getReminderFileContent. +-- ============================================= +CREATE PROCEDURE [dbo].[fds__getReminderFiles_ForBlobArchive] +AS +BEGIN + SET NOCOUNT ON; + + SELECT + [Id] + ,[Version] + ,[InvId] + ,[DocumentName] + FROM [dbo].[fds__reminder] + WHERE [file] IS NOT NULL + ORDER BY [Id]; +END diff --git a/Fuchs_Database/dbo/Tables/fds__reminder.sql b/Fuchs_Database/dbo/Tables/fds__reminder.sql index 7c3a3f5..d2d6a0a 100644 --- a/Fuchs_Database/dbo/Tables/fds__reminder.sql +++ b/Fuchs_Database/dbo/Tables/fds__reminder.sql @@ -1,29 +1,30 @@ CREATE TABLE [dbo].[fds__reminder] ( - [Id] VARCHAR (10) NOT NULL, - [Version] INT CONSTRAINT [DF_fds__reminder_Version] DEFAULT ((0)) 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] AS (CONVERT([numeric](10,3),isnull([amount],(0))-isnull([amount_payed],(0)))), - [subject] NVARCHAR (255) NULL, - [text] NVARCHAR (2000) NULL, - [IsSent] BIT CONSTRAINT [DF_fds__reminder_IsSent] DEFAULT ((0)) NOT NULL, - [IsFinal] AS (CONVERT([bit],case when [DateFinalized] IS NULL then (0) else (1) end)), - [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, - [file] VARBINARY (MAX) NULL, + [Id] VARCHAR (10) NOT NULL, + [Version] INT CONSTRAINT [DF_fds__reminder_Version] DEFAULT ((0)) 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] AS (CONVERT([numeric](10,3),isnull([amount],(0))-isnull([amount_payed],(0)))), + [subject] NVARCHAR (255) NULL, + [text] NVARCHAR (2000) NULL, + [IsSent] BIT CONSTRAINT [DF_fds__reminder_IsSent] DEFAULT ((0)) NOT NULL, + [IsFinal] AS (CONVERT([bit],case when [DateFinalized] IS NULL then (0) else (1) end)), + [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, + [file] VARBINARY (MAX) NULL, + [file_guid] UNIQUEIDENTIFIER CONSTRAINT [DF_fds__reminder_file_guid] DEFAULT (newid()) NOT NULL, CONSTRAINT [PK_fds__reminder] PRIMARY KEY CLUSTERED ([Id] ASC) ); diff --git a/Fuchs_Database/fds__getInvoiceFileContent.sql b/Fuchs_Database/fds__getInvoiceFileContent.sql new file mode 100644 index 0000000..e69de29 diff --git a/Fuchs_Database/fds__getInvoiceFiles_ForBlobArchive.sql b/Fuchs_Database/fds__getInvoiceFiles_ForBlobArchive.sql new file mode 100644 index 0000000..e69de29 diff --git a/Fuchs_Database/fds__getReminderFileContent.sql b/Fuchs_Database/fds__getReminderFileContent.sql new file mode 100644 index 0000000..e69de29 diff --git a/Fuchs_Database/fds__getReminderFiles_ForBlobArchive.sql b/Fuchs_Database/fds__getReminderFiles_ForBlobArchive.sql new file mode 100644 index 0000000..e69de29 diff --git a/Fuchs_Intranet.slnx b/Fuchs_Intranet.slnx index 2d27b9e..b490604 100644 --- a/Fuchs_Intranet.slnx +++ b/Fuchs_Intranet.slnx @@ -24,6 +24,16 @@ + + + + + + + + + + diff --git a/MFR_RESTClient/MFR_RESTClient.csproj b/MFR_RESTClient/MFR_RESTClient.csproj index 5340e2a..6d45cd3 100644 --- a/MFR_RESTClient/MFR_RESTClient.csproj +++ b/MFR_RESTClient/MFR_RESTClient.csproj @@ -26,17 +26,17 @@ - + - + - - - - + + + +