diff --git a/CLAUDE.md b/CLAUDE.md index afb6b8f..8c8a147 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,10 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co PlanTempus is a .NET 9 web application built with ASP.NET Core Razor Pages. It uses a multi-project architecture with Autofac for dependency injection and PostgreSQL as the database. +## Related Projects + +- **calpoc** = Calendar POC projekt located at `../Calendar` (TypeScript calendar component with offline-first architecture, drag-and-drop, NovaDI, EventBus). When user mentions "calpoc", refer to this folder. + ## Build and Development Commands ### Prerequisites diff --git a/Core/SeqLogging/SeqBackgroundService.cs b/Core/SeqLogging/SeqBackgroundService.cs deleted file mode 100644 index cef2cc1..0000000 --- a/Core/SeqLogging/SeqBackgroundService.cs +++ /dev/null @@ -1,88 +0,0 @@ -using Microsoft.ApplicationInsights; -using Microsoft.ApplicationInsights.Channel; -using Microsoft.ApplicationInsights.DataContracts; -using Microsoft.Extensions.Hosting; -using PlanTempus.Core.Telemetry; - -namespace PlanTempus.Core.SeqLogging -{ - public class SeqBackgroundService : BackgroundService - { - private readonly IMessageChannel _messageChannel; - private readonly TelemetryClient _telemetryClient; - private readonly SeqLogger _seqLogger; - - public SeqBackgroundService(TelemetryClient telemetryClient, - IMessageChannel messageChannel, - SeqLogger seqlogger) - { - _telemetryClient = telemetryClient; - _messageChannel = messageChannel; - _seqLogger = seqlogger; - - _telemetryClient.TrackTrace("SeqBackgroundService started"); - - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - while (!stoppingToken.IsCancellationRequested) - await foreach (var telemetry in _messageChannel.Reader.ReadAllAsync(stoppingToken)) - { - try - { - switch (telemetry) - { - case ExceptionTelemetry et: - await _seqLogger.LogAsync(et); - break; - - case TraceTelemetry et: - await _seqLogger.LogAsync(et); - break; - - case DependencyTelemetry et: - await _seqLogger.LogAsync(et); - break; - - case RequestTelemetry et: - await _seqLogger.LogAsync(et); - break; - - case EventTelemetry et: - await _seqLogger.LogAsync(et); - break; - - - default: - throw new NotSupportedException(telemetry.GetType().Name); - } - } - catch - { - throw; - //_telemetryClient.TrackException(ex); this is disabled for now, we need to think about the channel structure first - } - } - } - catch (Exception ex) - { - if (ex is not OperationCanceledException) - { - _telemetryClient.TrackException(ex); - throw; - } - - } - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - _telemetryClient.TrackTrace("StopAsync called: Service shutdown started"); - _messageChannel.Dispose(); - await base.StopAsync(cancellationToken); - } - } -} diff --git a/PTWork.code-workspace b/PTWork.code-workspace new file mode 100644 index 0000000..2ac8eb2 --- /dev/null +++ b/PTWork.code-workspace @@ -0,0 +1,13 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../Calendar" + } + ], + "settings": { + "liveServer.settings.port": 5501 + } +} \ No newline at end of file diff --git a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml new file mode 100644 index 0000000..9e3b36c --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml @@ -0,0 +1,94 @@ +@page "/" +@using PlanTempus.Application.Features.Dashboard.Pages +@model PlanTempus.Application.Features.Dashboard.Pages.IndexModel +@{ + ViewData["Title"] = "Dashboard"; +} + + + + + + 12 + Bookinger i dag + + + 4 gennemført, 2 i gang + + + + 8.450 kr + Forventet omsætning + + + +12% vs. gennemsnit + + + + 78% + Belægningsgrad + + + God kapacitet + + + + 4 + Kræver opmærksomhed + + + + + + + + + + + + AI Analyse + + + Godt i gang! 4 af 12 bookinger er gennemført. 2 er i gang nu, og 6 venter. + Forventet omsætning: 8.450 kr – allerede realiseret 2.150 kr. + + + + + + + + + + Dagens bookinger + + Se alle + + +

Booking oversigt kommer her...

+
+
+
+ + + + + + Hurtige handlinger + + + + + + Ny booking + + + + Ny kunde + + + + + +
+
diff --git a/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml.cs b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml.cs new file mode 100644 index 0000000..c822af3 --- /dev/null +++ b/PlanTempus.Application/Features/Dashboard/Pages/Index.cshtml.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace PlanTempus.Application.Features.Dashboard.Pages; + +public class IndexModel : PageModel +{ + public void OnGet() + { + } +} diff --git a/PlanTempus.Application/Features/Localization/Models/SupportedCulture.cs b/PlanTempus.Application/Features/Localization/Models/SupportedCulture.cs new file mode 100644 index 0000000..da758e7 --- /dev/null +++ b/PlanTempus.Application/Features/Localization/Models/SupportedCulture.cs @@ -0,0 +1,8 @@ +namespace PlanTempus.Application.Features.Localization.Models; + +public class SupportedCulture +{ + public required string Code { get; set; } + public required string Name { get; set; } + public required string NativeName { get; set; } +} diff --git a/PlanTempus.Application/Features/Localization/Services/ILocalizationService.cs b/PlanTempus.Application/Features/Localization/Services/ILocalizationService.cs new file mode 100644 index 0000000..c689ec8 --- /dev/null +++ b/PlanTempus.Application/Features/Localization/Services/ILocalizationService.cs @@ -0,0 +1,10 @@ +using PlanTempus.Application.Features.Localization.Models; + +namespace PlanTempus.Application.Features.Localization.Services; + +public interface ILocalizationService +{ + string Get(string key, string? culture = null); + string CurrentCulture { get; } + IEnumerable GetSupportedCultures(); +} diff --git a/PlanTempus.Application/Features/Localization/Services/JsonLocalizationService.cs b/PlanTempus.Application/Features/Localization/Services/JsonLocalizationService.cs new file mode 100644 index 0000000..0c96748 --- /dev/null +++ b/PlanTempus.Application/Features/Localization/Services/JsonLocalizationService.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using PlanTempus.Application.Features.Localization.Models; + +namespace PlanTempus.Application.Features.Localization.Services; + +public class JsonLocalizationService : ILocalizationService +{ + private readonly string _translationsPath; + + public JsonLocalizationService(IWebHostEnvironment env) + { + _translationsPath = Path.Combine(env.ContentRootPath, "Features", "Localization", "Translations"); + } + + public string CurrentCulture => "da"; + + public string Get(string key, string? culture = null) + { + culture ??= CurrentCulture; + var filePath = Path.Combine(_translationsPath, $"{culture}.json"); + + if (!File.Exists(filePath)) + return key; + + var json = File.ReadAllText(filePath); + var doc = JsonDocument.Parse(json); + + var parts = key.Split('.'); + JsonElement current = doc.RootElement; + + foreach (var part in parts) + if (!current.TryGetProperty(part, out current)) + return key; + + return current.GetString() ?? key; + } + + public IEnumerable GetSupportedCultures() + { + return new List + { + new() { Code = "da", Name = "Danish", NativeName = "Dansk" }, + new() { Code = "en", Name = "English", NativeName = "English" } + }; + } +} diff --git a/PlanTempus.Application/Features/Localization/Translations/da.json b/PlanTempus.Application/Features/Localization/Translations/da.json new file mode 100644 index 0000000..c569ae6 --- /dev/null +++ b/PlanTempus.Application/Features/Localization/Translations/da.json @@ -0,0 +1,33 @@ +{ + "menu": { + "home": "Dashboard", + "calendar": "Kalender", + "pos": "Kasse", + "products": "Produkter & Lager", + "suppliers": "Leverandører", + "customers": "Kunder", + "employees": "Medarbejdere", + "reports": "Statistik & Rapporter", + "settings": "Indstillinger", + "account": "Abonnement & Konto" + }, + "groups": { + "dashboard": "Dashboard", + "data": "Data", + "analytics": "Analyse", + "system": "System" + }, + "common": { + "save": "Gem", + "cancel": "Annuller", + "search": "Søg", + "close": "Luk", + "delete": "Slet", + "edit": "Rediger", + "add": "Tilføj" + }, + "sidebar": { + "lockScreen": "Lås skærm", + "appName": "Salon OS" + } +} diff --git a/PlanTempus.Application/Features/Localization/Translations/en.json b/PlanTempus.Application/Features/Localization/Translations/en.json new file mode 100644 index 0000000..a66a7b7 --- /dev/null +++ b/PlanTempus.Application/Features/Localization/Translations/en.json @@ -0,0 +1,33 @@ +{ + "menu": { + "home": "Dashboard", + "calendar": "Calendar", + "pos": "Point of Sale", + "products": "Products & Inventory", + "suppliers": "Suppliers", + "customers": "Customers", + "employees": "Employees", + "reports": "Statistics & Reports", + "settings": "Settings", + "account": "Subscription & Account" + }, + "groups": { + "dashboard": "Dashboard", + "data": "Data", + "analytics": "Analytics", + "system": "System" + }, + "common": { + "save": "Save", + "cancel": "Cancel", + "search": "Search", + "close": "Close", + "delete": "Delete", + "edit": "Edit", + "add": "Add" + }, + "sidebar": { + "lockScreen": "Lock screen", + "appName": "Salon OS" + } +} diff --git a/PlanTempus.Application/Features/Menu/Models/MenuGroup.cs b/PlanTempus.Application/Features/Menu/Models/MenuGroup.cs new file mode 100644 index 0000000..2b68088 --- /dev/null +++ b/PlanTempus.Application/Features/Menu/Models/MenuGroup.cs @@ -0,0 +1,12 @@ +namespace PlanTempus.Application.Features.Menu.Models; + +/// +/// Represents a group of menu items (e.g., "Dashboard", "Data", "System"). +/// +public class MenuGroup +{ + public required string Id { get; set; } + public required string Label { get; set; } + public int SortOrder { get; set; } + public List Items { get; set; } = new(); +} diff --git a/PlanTempus.Application/Features/Menu/Models/MenuItem.cs b/PlanTempus.Application/Features/Menu/Models/MenuItem.cs new file mode 100644 index 0000000..6996726 --- /dev/null +++ b/PlanTempus.Application/Features/Menu/Models/MenuItem.cs @@ -0,0 +1,15 @@ +namespace PlanTempus.Application.Features.Menu.Models; + +/// +/// Represents a single menu item in the sidebar. +/// +public class MenuItem +{ + public required string Id { get; set; } + public required string Label { get; set; } + public required string Icon { get; set; } + public required string Url { get; set; } + public UserRole MinimumRole { get; set; } = UserRole.Staff; + public int SortOrder { get; set; } + public bool IsActive { get; set; } +} diff --git a/PlanTempus.Application/Features/Menu/Models/UserRole.cs b/PlanTempus.Application/Features/Menu/Models/UserRole.cs new file mode 100644 index 0000000..44d5aea --- /dev/null +++ b/PlanTempus.Application/Features/Menu/Models/UserRole.cs @@ -0,0 +1,11 @@ +namespace PlanTempus.Application.Features.Menu.Models; + +/// +/// User roles for menu visibility. Higher value = more access. +/// +public enum UserRole +{ + Staff = 0, + Manager = 1, + Admin = 2 +} diff --git a/PlanTempus.Application/Features/Menu/Services/IMenuService.cs b/PlanTempus.Application/Features/Menu/Services/IMenuService.cs new file mode 100644 index 0000000..a69c850 --- /dev/null +++ b/PlanTempus.Application/Features/Menu/Services/IMenuService.cs @@ -0,0 +1,14 @@ +using PlanTempus.Application.Features.Menu.Models; + +namespace PlanTempus.Application.Features.Menu.Services; + +/// +/// Service for retrieving menu structure based on user role. +/// +public interface IMenuService +{ + /// + /// Get menu groups filtered by user role. + /// + List GetMenuForRole(UserRole role, string? currentUrl = null); +} diff --git a/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs b/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs new file mode 100644 index 0000000..d42f628 --- /dev/null +++ b/PlanTempus.Application/Features/Menu/Services/MockMenuService.cs @@ -0,0 +1,187 @@ +using PlanTempus.Application.Features.Localization.Services; +using PlanTempus.Application.Features.Menu.Models; + +namespace PlanTempus.Application.Features.Menu.Services; + +/// +/// Mock implementation of IMenuService with hardcoded menu data. +/// +public class MockMenuService : IMenuService +{ + private readonly ILocalizationService _localize; + + public MockMenuService(ILocalizationService localize) + { + _localize = localize; + } + + public List GetMenuForRole(UserRole role, string? currentUrl = null) + { + var allGroups = GetAllMenuGroups(); + + return allGroups + .Select(g => new MenuGroup + { + Id = g.Id, + Label = _localize.Get($"groups.{g.Id}"), + SortOrder = g.SortOrder, + Items = g.Items + .Where(i => role >= i.MinimumRole) + .Select(i => new MenuItem + { + Id = i.Id, + Label = _localize.Get($"menu.{i.Id}"), + Icon = i.Icon, + Url = i.Url, + MinimumRole = i.MinimumRole, + SortOrder = i.SortOrder, + IsActive = currentUrl != null && i.Url.Equals(currentUrl, StringComparison.OrdinalIgnoreCase) + }) + .OrderBy(i => i.SortOrder) + .ToList() + }) + .Where(g => g.Items.Any()) + .OrderBy(g => g.SortOrder) + .ToList(); + } + + private static List GetAllMenuGroups() + { + return new List + { + // DASHBOARD GROUP + new MenuGroup + { + Id = "dashboard", + Label = "Dashboard", + SortOrder = 1, + Items = new List + { + new MenuItem + { + Id = "home", + Label = "Dashboard", + Icon = "ph-squares-four", + Url = "/", + MinimumRole = UserRole.Staff, + SortOrder = 1 + }, + new MenuItem + { + Id = "calendar", + Label = "Kalender", + Icon = "ph-calendar", + Url = "/poc-calendar.html", + MinimumRole = UserRole.Staff, + SortOrder = 2 + }, + new MenuItem + { + Id = "pos", + Label = "Kasse", + Icon = "ph-device-mobile", + Url = "/pos", + MinimumRole = UserRole.Staff, + SortOrder = 3 + } + } + }, + + // DATA GROUP + new MenuGroup + { + Id = "data", + Label = "Data", + SortOrder = 2, + Items = new List + { + new MenuItem + { + Id = "products", + Label = "Produkter & Lager", + Icon = "ph-package", + Url = "/poc-produkter.html", + MinimumRole = UserRole.Manager, + SortOrder = 1 + }, + new MenuItem + { + Id = "suppliers", + Label = "Leverandører", + Icon = "ph-truck", + Url = "/poc-leverandoerer.html", + MinimumRole = UserRole.Manager, + SortOrder = 2 + }, + new MenuItem + { + Id = "customers", + Label = "Kunder", + Icon = "ph-users", + Url = "/customers", + MinimumRole = UserRole.Staff, + SortOrder = 3 + }, + new MenuItem + { + Id = "employees", + Label = "Medarbejdere", + Icon = "ph-user", + Url = "/poc-medarbejdere.html", + MinimumRole = UserRole.Manager, + SortOrder = 4 + } + } + }, + + // ANALYSE GROUP + new MenuGroup + { + Id = "analytics", + Label = "Analyse", + SortOrder = 3, + Items = new List + { + new MenuItem + { + Id = "reports", + Label = "Statistik & Rapporter", + Icon = "ph-chart-bar", + Url = "/reports", + MinimumRole = UserRole.Manager, + SortOrder = 1 + } + } + }, + + // SYSTEM GROUP + new MenuGroup + { + Id = "system", + Label = "System", + SortOrder = 4, + Items = new List + { + new MenuItem + { + Id = "settings", + Label = "Indstillinger", + Icon = "ph-gear", + Url = "/poc-indstillinger.html", + MinimumRole = UserRole.Admin, + SortOrder = 1 + }, + new MenuItem + { + Id = "account", + Label = "Abonnement & Konto", + Icon = "ph-credit-card", + Url = "/poc-konto.html", + MinimumRole = UserRole.Admin, + SortOrder = 2 + } + } + } + }; + } +} diff --git a/PlanTempus.Application/Features/Menu/SideMenuViewComponent.cs b/PlanTempus.Application/Features/Menu/SideMenuViewComponent.cs new file mode 100644 index 0000000..5c3c75e --- /dev/null +++ b/PlanTempus.Application/Features/Menu/SideMenuViewComponent.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using PlanTempus.Application.Features.Menu.Services; +using PlanTempus.Application.Features.Menu; +using PlanTempus.Application.Features.Menu.Models; + +namespace PlanTempus.Application.Features.Menu; + +/// +/// ViewComponent for rendering the side menu based on user role. +/// +public class SideMenuViewComponent : ViewComponent +{ + private readonly IMenuService _menuService; + + public SideMenuViewComponent(IMenuService menuService) + { + _menuService = menuService; + } + + public IViewComponentResult Invoke(UserRole? role = null) + { + // Default to Admin for demo (in real app, get from auth) + var userRole = role ?? UserRole.Admin; + + var currentUrl = HttpContext.Request.Path.Value; + var groups = _menuService.GetMenuForRole(userRole, currentUrl); + + var viewModel = new SideMenuViewModel + { + Groups = groups, + CurrentUserRole = userRole + }; + + return View(viewModel); + } +} diff --git a/PlanTempus.Application/Features/Menu/SideMenuViewModel.cs b/PlanTempus.Application/Features/Menu/SideMenuViewModel.cs new file mode 100644 index 0000000..1a5c408 --- /dev/null +++ b/PlanTempus.Application/Features/Menu/SideMenuViewModel.cs @@ -0,0 +1,12 @@ +using PlanTempus.Application.Features.Menu.Models; + +namespace PlanTempus.Application.Features.Menu; + +/// +/// ViewModel for the side menu partial view. +/// +public class SideMenuViewModel +{ + public required List Groups { get; set; } + public UserRole CurrentUserRole { get; set; } +} diff --git a/PlanTempus.Application/Features/Shared/Components/SideMenu/Default.cshtml b/PlanTempus.Application/Features/Shared/Components/SideMenu/Default.cshtml new file mode 100644 index 0000000..2514a4b --- /dev/null +++ b/PlanTempus.Application/Features/Shared/Components/SideMenu/Default.cshtml @@ -0,0 +1,40 @@ +@using PlanTempus.Application.Features.Menu +@model SideMenuViewModel + + + + + Salon OS + + + + + + + @foreach (var group in Model.Groups) + { + + @group.Label + @foreach (var item in group.Items) + { + + + @item.Label + + } + + } + + + + + + Lås skærm + + + + + + diff --git a/PlanTempus.Application/Features/Shared/_ProfileDrawer.cshtml b/PlanTempus.Application/Features/Shared/_ProfileDrawer.cshtml new file mode 100644 index 0000000..9e42620 --- /dev/null +++ b/PlanTempus.Application/Features/Shared/_ProfileDrawer.cshtml @@ -0,0 +1,49 @@ + + + Profil + + + + + + + + MJ + Maria Jensen + maria@salon.dk + + + + + + + + Min profil + + + + Indstillinger + + + + + + + + + Mørk tilstand + + + + + + + + + + + + Log ud + + + diff --git a/PlanTempus.Application/Features/Shared/_TopBar.cshtml b/PlanTempus.Application/Features/Shared/_TopBar.cshtml new file mode 100644 index 0000000..41b140a --- /dev/null +++ b/PlanTempus.Application/Features/Shared/_TopBar.cshtml @@ -0,0 +1,26 @@ + + + + + ⌘K + + + + + + + 3 + + + + + + + MJ + + Maria Jensen + Administrator + + + + diff --git a/PlanTempus.Application/Features/_Shared/Components/SideMenu/Default.cshtml b/PlanTempus.Application/Features/_Shared/Components/SideMenu/Default.cshtml new file mode 100644 index 0000000..ae7de86 --- /dev/null +++ b/PlanTempus.Application/Features/_Shared/Components/SideMenu/Default.cshtml @@ -0,0 +1,39 @@ +@model PlanTempus.Application.Features.Menu.SideMenuViewModel + + + + + Salon OS + + + + + + + @foreach (var group in Model.Groups) + { + + @group.Label + @foreach (var item in group.Items) + { + + + @item.Label + + } + + } + + + + + + Lås skærm + + + + + + diff --git a/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml new file mode 100644 index 0000000..e8bb2e3 --- /dev/null +++ b/PlanTempus.Application/Features/_Shared/Pages/_Layout.cshtml @@ -0,0 +1,38 @@ + + + + + + @ViewData["Title"] - Salon OS + + + + + + + + + + + + + + @await RenderSectionAsync("Styles", required: false) + + + + @await Component.InvokeAsync("SideMenu") + + + + @RenderBody() + + + + + + + + @await RenderSectionAsync("Scripts", required: false) + + diff --git a/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml new file mode 100644 index 0000000..9e42620 --- /dev/null +++ b/PlanTempus.Application/Features/_Shared/Pages/_ProfileDrawer.cshtml @@ -0,0 +1,49 @@ + + + Profil + + + + + + + + MJ + Maria Jensen + maria@salon.dk + + + + + + + + Min profil + + + + Indstillinger + + + + + + + + + Mørk tilstand + + + + + + + + + + + + Log ud + + + diff --git a/PlanTempus.Application/Features/_Shared/Pages/_TopBar.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_TopBar.cshtml new file mode 100644 index 0000000..41b140a --- /dev/null +++ b/PlanTempus.Application/Features/_Shared/Pages/_TopBar.cshtml @@ -0,0 +1,26 @@ + + + + + ⌘K + + + + + + + 3 + + + + + + + MJ + + Maria Jensen + Administrator + + + + diff --git a/PlanTempus.Application/Features/_Shared/Pages/_ViewImports.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..bfd56f8 --- /dev/null +++ b/PlanTempus.Application/Features/_Shared/Pages/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using PlanTempus.Application +@namespace PlanTempus.Application.Features +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, PlanTempus.Application diff --git a/PlanTempus.Application/Features/_Shared/Pages/_ViewStart.cshtml b/PlanTempus.Application/Features/_Shared/Pages/_ViewStart.cshtml new file mode 100644 index 0000000..f9f7d10 --- /dev/null +++ b/PlanTempus.Application/Features/_Shared/Pages/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Features/_Shared/Pages/_Layout.cshtml"; +} diff --git a/PlanTempus.Application/Features/_Shared/TagHelpers/LocalizeTagHelper.cs b/PlanTempus.Application/Features/_Shared/TagHelpers/LocalizeTagHelper.cs new file mode 100644 index 0000000..9e77cf0 --- /dev/null +++ b/PlanTempus.Application/Features/_Shared/TagHelpers/LocalizeTagHelper.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using PlanTempus.Application.Features.Localization.Services; + +namespace PlanTempus.Application.Features._Shared.TagHelpers; + +[HtmlTargetElement(Attributes = "localize")] +public class LocalizeTagHelper : TagHelper +{ + private readonly ILocalizationService _localize; + + public LocalizeTagHelper(ILocalizationService localize) + { + _localize = localize; + } + + [HtmlAttributeName("localize")] + public string Key { get; set; } = string.Empty; + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + var translated = _localize.Get(Key); + + if (!string.IsNullOrEmpty(translated) && translated != Key) + output.Content.SetContent(translated); + + output.Attributes.RemoveAll("localize"); + } +} diff --git a/PlanTempus.Application/Features/_ViewImports.cshtml b/PlanTempus.Application/Features/_ViewImports.cshtml new file mode 100644 index 0000000..bfd56f8 --- /dev/null +++ b/PlanTempus.Application/Features/_ViewImports.cshtml @@ -0,0 +1,4 @@ +@using PlanTempus.Application +@namespace PlanTempus.Application.Features +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, PlanTempus.Application diff --git a/PlanTempus.Application/Features/_ViewStart.cshtml b/PlanTempus.Application/Features/_ViewStart.cshtml new file mode 100644 index 0000000..f9f7d10 --- /dev/null +++ b/PlanTempus.Application/Features/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Features/_Shared/Pages/_Layout.cshtml"; +} diff --git a/PlanTempus.Application/PlanTempus.Application.csproj b/PlanTempus.Application/PlanTempus.Application.csproj new file mode 100644 index 0000000..b9ee8e2 --- /dev/null +++ b/PlanTempus.Application/PlanTempus.Application.csproj @@ -0,0 +1,19 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + \ No newline at end of file diff --git a/PlanTempus.Application/Program.cs b/PlanTempus.Application/Program.cs new file mode 100644 index 0000000..e839d00 --- /dev/null +++ b/PlanTempus.Application/Program.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.RazorPages; +using PlanTempus.Application.Features.Localization.Services; +using PlanTempus.Application.Features.Menu.Services; + +var builder = WebApplication.CreateBuilder(args); + +// Add Razor Pages with feature-based structure +builder.Services.AddRazorPages(options => +{ + options.RootDirectory = "/Features"; +}) +.AddRazorOptions(options => +{ + // View locations for partials and ViewComponents + options.ViewLocationFormats.Add("/Features/_Shared/Pages/{0}.cshtml"); + options.ViewLocationFormats.Add("/Features/_Shared/Components/{1}/{0}.cshtml"); + +}); + +// Register application services +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +// Developer exception page for debugging +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); +} + +// Serve static files from wwwroot +app.UseStaticFiles(); + +// Configure routing +app.UseRouting(); + +// Map Razor Pages +app.MapRazorPages(); + +app.Run("http://localhost:8000"); + +// Note: Set ASPNETCORE_ENVIRONMENT=Development for detailed error pages diff --git a/PlanTempus.Application/Properties/launchSettings.json b/PlanTempus.Application/Properties/launchSettings.json new file mode 100644 index 0000000..8f87adf --- /dev/null +++ b/PlanTempus.Application/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "PlanTempus.Application": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:55726;http://localhost:55727" + } + } +} \ No newline at end of file diff --git a/PlanTempus.Application/build.js b/PlanTempus.Application/build.js new file mode 100644 index 0000000..4aa944e --- /dev/null +++ b/PlanTempus.Application/build.js @@ -0,0 +1,24 @@ +import * as esbuild from 'esbuild'; + +async function build() { + try { + await esbuild.build({ + entryPoints: ['wwwroot/ts/app.ts'], + bundle: true, + outfile: 'wwwroot/js/app.js', + format: 'esm', + sourcemap: 'inline', + target: 'es2020', + minify: false, + keepNames: true, + platform: 'browser' + }); + + console.log('App bundle created: wwwroot/js/app.js'); + } catch (error) { + console.error('Build failed:', error); + process.exit(1); + } +} + +build(); diff --git a/PlanTempus.Application/wwwroot/css/app-layout.css b/PlanTempus.Application/wwwroot/css/app-layout.css new file mode 100644 index 0000000..2f57b74 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/app-layout.css @@ -0,0 +1,50 @@ +/** + * App Layout - Main Grid Structure + * + * Definerer den overordnede app-struktur med sidebar og main content + */ + +/* =========================================== + MAIN APP GRID + =========================================== */ +swp-app-layout { + display: grid; + grid-template-columns: var(--side-menu-width) 1fr; + grid-template-rows: var(--topbar-height) 1fr; + height: 100vh; + transition: grid-template-columns var(--transition-normal); +} + +/* =========================================== + COLLAPSED MENU STATE + =========================================== */ +swp-app-layout.menu-collapsed { + grid-template-columns: var(--side-menu-width-collapsed) 1fr; +} + +/* =========================================== + MAIN CONTENT AREA + =========================================== */ +swp-main-content { + display: block; + overflow-y: auto; + background: var(--color-background); +} + +/* =========================================== + DRAWER OVERLAY + =========================================== */ +swp-drawer-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: var(--z-overlay); + opacity: 0; + visibility: hidden; + transition: opacity var(--transition-normal), visibility var(--transition-normal); +} + +swp-drawer-overlay.active { + opacity: 1; + visibility: visible; +} diff --git a/PlanTempus.Application/wwwroot/css/base.css b/PlanTempus.Application/wwwroot/css/base.css new file mode 100644 index 0000000..00669dc --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/base.css @@ -0,0 +1,118 @@ +/** + * Base Styles - Reset & Global Elements + * + * Normalization og grundlæggende styling + */ + +/* =========================================== + FONT FACES + =========================================== */ +@font-face { + font-family: 'Poppins'; + src: url('../fonts/Poppins-Regular.woff') format('woff'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Poppins'; + src: url('../fonts/Poppins-Medium.woff') format('woff'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Poppins'; + src: url('../fonts/Poppins-SemiBold.woff') format('woff'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Poppins'; + src: url('../fonts/Poppins-Bold.woff') format('woff'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +/* =========================================== + RESET + =========================================== */ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + height: 100%; +} + +/* =========================================== + BASE ELEMENTS + =========================================== */ +body { + font-family: var(--font-family); + font-size: var(--font-size-base); + color: var(--color-text); + background: var(--color-background); + line-height: var(--line-height-normal); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Links */ +a { + color: var(--color-teal); + text-decoration: none; + transition: color var(--transition-fast); +} + +a:hover { + text-decoration: underline; +} + +/* Lists */ +ul, ol { + list-style: none; +} + +/* Images */ +img { + max-width: 100%; + height: auto; + display: block; +} + +/* Buttons */ +button { + font-family: inherit; + cursor: pointer; +} + +/* Inputs */ +input, +textarea, +select { + font-family: inherit; + font-size: inherit; +} + +/* Focus visible */ +:focus-visible { + outline: 2px solid var(--color-teal); + outline-offset: 2px; +} + +/* Selection */ +::selection { + background: var(--color-teal-light); + color: var(--color-text); +} diff --git a/PlanTempus.Application/wwwroot/css/design-system.css b/PlanTempus.Application/wwwroot/css/design-system.css new file mode 100644 index 0000000..beb61ec --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/design-system.css @@ -0,0 +1,163 @@ +/** + * SWP Design System - CSS Variables + * + * Dette er den centrale definition af alle design tokens. + * Alle farver, fonts og layout-variabler defineres her. + */ + +/* =========================================== + COLOR PALETTE - Light Mode (Default) + =========================================== */ +:root { + /* Surfaces */ + --color-surface: #fff; + --color-background: #f5f5f5; + --color-background-hover: #f0f0f0; + --color-background-alt: #fafafa; + + /* Borders */ + --color-border: #e0e0e0; + --color-border-light: #f0f0f0; + + /* Text */ + --color-text: #333; + --color-text-secondary: #666; + --color-text-muted: #999; + + /* Brand Colors */ + --color-teal: #00897b; + --color-teal-light: color-mix(in srgb, var(--color-teal) 10%, transparent); + + /* Semantic Colors */ + --color-blue: #1976d2; + --color-green: #43a047; + --color-amber: #f59e0b; + --color-red: #e53935; + --color-purple: #8b5cf6; +} + +/* =========================================== + COLOR PALETTE - Dark Mode (System) + =========================================== */ +@media (prefers-color-scheme: dark) { + :root:not(.light-mode) { + --color-surface: #1e1e1e; + --color-background: #121212; + --color-background-hover: #2a2a2a; + --color-background-alt: #1a1a1a; + + --color-border: #333; + --color-border-light: #2a2a2a; + + --color-text: #e0e0e0; + --color-text-secondary: #999; + --color-text-muted: #666; + + --color-teal: #26a69a; + --color-blue: #42a5f5; + --color-green: #66bb6a; + --color-amber: #ffb74d; + --color-red: #ef5350; + --color-purple: #a78bfa; + } +} + +/* =========================================== + COLOR PALETTE - Dark Mode (Manual) + =========================================== */ +:root.dark-mode { + --color-surface: #1e1e1e; + --color-background: #121212; + --color-background-hover: #2a2a2a; + --color-background-alt: #1a1a1a; + + --color-border: #333; + --color-border-light: #2a2a2a; + + --color-text: #e0e0e0; + --color-text-secondary: #999; + --color-text-muted: #666; + + --color-teal: #26a69a; + --color-blue: #42a5f5; + --color-green: #66bb6a; + --color-amber: #ffb74d; + --color-red: #ef5350; + --color-purple: #a78bfa; +} + +/* =========================================== + TYPOGRAPHY + =========================================== */ +:root { + --font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + /* Font Sizes */ + --font-size-xs: 11px; + --font-size-sm: 12px; + --font-size-base: 14px; + --font-size-md: 13px; + --font-size-lg: 16px; + --font-size-xl: 22px; + + /* Line Heights */ + --line-height-tight: 1.25; + --line-height-normal: 1.5; + --line-height-relaxed: 1.75; +} + +/* =========================================== + SPACING + =========================================== */ +:root { + --spacing-1: 4px; + --spacing-2: 8px; + --spacing-3: 12px; + --spacing-4: 16px; + --spacing-5: 20px; + --spacing-6: 24px; + --spacing-8: 32px; +} + +/* =========================================== + LAYOUT + =========================================== */ +:root { + --side-menu-width: 240px; + --side-menu-width-collapsed: 64px; + --topbar-height: 56px; + --page-max-width: 1400px; + --border-radius: 6px; + --border-radius-lg: 8px; +} + +/* =========================================== + TRANSITIONS + =========================================== */ +:root { + --transition-fast: 150ms ease; + --transition-normal: 200ms ease; + --transition-slow: 300ms ease; +} + +/* =========================================== + Z-INDEX LAYERS + =========================================== */ +:root { + --z-dropdown: 100; + --z-sticky: 200; + --z-overlay: 900; + --z-drawer: 1000; + --z-modal: 1100; + --z-tooltip: 1200; +} + +/* =========================================== + SHADOWS + =========================================== */ +:root { + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 2px 8px rgba(0, 0, 0, 0.1); + --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.15); +} diff --git a/PlanTempus.Application/wwwroot/css/drawers.css b/PlanTempus.Application/wwwroot/css/drawers.css new file mode 100644 index 0000000..2805da5 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/drawers.css @@ -0,0 +1,258 @@ +/** + * Drawers - Slide-in Panels + * + * Profile drawer, notifications drawer, etc. + */ + +/* =========================================== + BASE DRAWER + =========================================== */ +swp-profile-drawer, +swp-notification-drawer, +swp-todo-drawer { + position: fixed; + top: 0; + right: 0; + width: 320px; + height: 100vh; + background: var(--color-surface); + border-left: 1px solid var(--color-border); + box-shadow: var(--shadow-lg); + z-index: var(--z-drawer); + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform var(--transition-normal); +} + +swp-profile-drawer.active, +swp-notification-drawer.active, +swp-todo-drawer.active { + transform: translateX(0); +} + +/* =========================================== + DRAWER HEADER + =========================================== */ +swp-drawer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-4) var(--spacing-5); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +swp-drawer-title { + font-size: var(--font-size-lg); + font-weight: 600; + color: var(--color-text); +} + +swp-drawer-close { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + border-radius: var(--border-radius); + cursor: pointer; + color: var(--color-text-secondary); + transition: all var(--transition-fast); +} + +swp-drawer-close:hover { + background: var(--color-background-hover); + color: var(--color-text); +} + +swp-drawer-close i { + font-size: 20px; +} + +/* =========================================== + DRAWER CONTENT + =========================================== */ +swp-drawer-content { + flex: 1; + overflow-y: auto; + padding: var(--spacing-5); +} + +swp-drawer-divider { + height: 1px; + background: var(--color-border); + margin: var(--spacing-4) 0; +} + +/* =========================================== + PROFILE SECTION + =========================================== */ +swp-profile-section { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: var(--spacing-4) 0; +} + +swp-profile-avatar-large { + width: 64px; + height: 64px; + border-radius: 50%; + background: var(--color-teal); + color: white; + font-size: 24px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: var(--spacing-3); +} + +swp-profile-name-large { + font-size: var(--font-size-lg); + font-weight: 600; + color: var(--color-text); + margin-bottom: var(--spacing-1); +} + +swp-profile-email { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +/* =========================================== + DRAWER MENU + =========================================== */ +swp-drawer-menu { + display: flex; + flex-direction: column; + gap: var(--spacing-1); +} + +swp-drawer-menu-item { + display: flex; + align-items: center; + gap: var(--spacing-3); + padding: var(--spacing-3) var(--spacing-3); + border-radius: var(--border-radius); + cursor: pointer; + transition: background var(--transition-fast); + color: var(--color-text); +} + +swp-drawer-menu-item:hover { + background: var(--color-background-hover); +} + +swp-drawer-menu-item i { + font-size: 20px; + color: var(--color-text-secondary); +} + +/* =========================================== + THEME TOGGLE + =========================================== */ +swp-theme-toggle { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-3); + border-radius: var(--border-radius); + background: var(--color-background); +} + +swp-theme-label { + display: flex; + align-items: center; + gap: var(--spacing-3); + color: var(--color-text); +} + +swp-theme-label i { + font-size: 20px; + color: var(--color-text-secondary); +} + +swp-toggle-switch { + position: relative; + width: 44px; + height: 24px; +} + +swp-toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} + +swp-toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background: var(--color-border); + border-radius: 12px; + transition: background var(--transition-fast); +} + +swp-toggle-slider::before { + content: ''; + position: absolute; + width: 18px; + height: 18px; + left: 3px; + bottom: 3px; + background: white; + border-radius: 50%; + transition: transform var(--transition-fast); +} + +swp-toggle-switch input:checked + swp-toggle-slider { + background: var(--color-teal); +} + +swp-toggle-switch input:checked + swp-toggle-slider::before { + transform: translateX(20px); +} + +/* =========================================== + DRAWER FOOTER + =========================================== */ +swp-drawer-footer { + padding: var(--spacing-4) var(--spacing-5); + border-top: 1px solid var(--color-border); + flex-shrink: 0; +} + +swp-drawer-action { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-2); + width: 100%; + padding: var(--spacing-3); + font-size: var(--font-size-base); + font-family: var(--font-family); + color: var(--color-text-secondary); + background: transparent; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + cursor: pointer; + transition: all var(--transition-fast); +} + +swp-drawer-action:hover { + background: var(--color-background-hover); +} + +swp-drawer-action.logout:hover { + color: var(--color-red); + border-color: var(--color-red); +} + +swp-drawer-action i { + font-size: 18px; +} diff --git a/PlanTempus.Application/wwwroot/css/page.css b/PlanTempus.Application/wwwroot/css/page.css new file mode 100644 index 0000000..cf4b9c7 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/page.css @@ -0,0 +1,204 @@ +/** + * Page Layout - Content Area Structure + * + * Page container, headers, cards og grid layouts + */ + +/* =========================================== + PAGE CONTAINER + =========================================== */ +swp-page-container { + display: block; + max-width: var(--page-max-width); + margin: 0 auto; + padding: var(--spacing-6); +} + +/* =========================================== + PAGE HEADER + =========================================== */ +swp-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: var(--spacing-6); +} + +swp-page-title h1 { + font-size: 24px; + font-weight: 600; + color: var(--color-text); + margin-bottom: var(--spacing-1); +} + +swp-page-title p { + font-size: var(--font-size-base); + color: var(--color-text-secondary); +} + +swp-page-actions { + display: flex; + gap: var(--spacing-2); +} + +/* =========================================== + CARDS + =========================================== */ +swp-card { + display: block; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-lg); + margin-bottom: var(--spacing-5); +} + +swp-card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--spacing-4) var(--spacing-5); + border-bottom: 1px solid var(--color-border); +} + +swp-card-title { + display: flex; + align-items: center; + gap: var(--spacing-2); + font-size: var(--font-size-base); + font-weight: 600; + color: var(--color-text); +} + +swp-card-title i { + font-size: 20px; + color: var(--color-text-secondary); +} + +swp-card-action { + font-size: var(--font-size-md); + color: var(--color-teal); + cursor: pointer; + transition: opacity var(--transition-fast); +} + +swp-card-action:hover { + opacity: 0.8; +} + +swp-card-content { + padding: var(--spacing-5); +} + +/* =========================================== + DASHBOARD GRID + =========================================== */ +swp-dashboard-grid { + display: grid; + grid-template-columns: 1fr 350px; + gap: var(--spacing-5); +} + +swp-main-column { + display: flex; + flex-direction: column; +} + +swp-side-column { + display: flex; + flex-direction: column; +} + +/* =========================================== + AI INSIGHT + =========================================== */ +swp-ai-insight { + display: block; + padding: var(--spacing-4) var(--spacing-5); + background: linear-gradient(135deg, + color-mix(in srgb, var(--color-purple) 8%, transparent), + color-mix(in srgb, var(--color-teal) 8%, transparent) + ); + border-radius: var(--border-radius); +} + +swp-ai-header { + display: flex; + align-items: center; + gap: var(--spacing-2); + margin-bottom: var(--spacing-2); + font-size: var(--font-size-sm); + font-weight: 500; + color: var(--color-purple); +} + +swp-ai-header i { + font-size: 16px; +} + +swp-ai-text { + font-size: var(--font-size-base); + color: var(--color-text); + line-height: var(--line-height-relaxed); +} + +/* =========================================== + QUICK ACTIONS + =========================================== */ +swp-quick-actions { + display: flex; + flex-direction: column; + gap: var(--spacing-2); +} + +swp-quick-action-btn { + display: flex; + align-items: center; + gap: var(--spacing-2); + padding: var(--spacing-3); + font-size: var(--font-size-base); + font-family: var(--font-family); + color: var(--color-text); + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + cursor: pointer; + transition: all var(--transition-fast); +} + +swp-quick-action-btn:hover { + background: var(--color-background-hover); + border-color: var(--color-teal); + color: var(--color-teal); +} + +swp-quick-action-btn i { + font-size: 18px; +} + +/* =========================================== + RESPONSIVE + =========================================== */ +@media (max-width: 1200px) { + swp-dashboard-grid { + grid-template-columns: 1fr; + } + + swp-side-column { + order: -1; + } +} + +@media (max-width: 768px) { + swp-page-container { + padding: var(--spacing-4); + } + + swp-page-header { + flex-direction: column; + gap: var(--spacing-4); + } + + swp-page-actions { + width: 100%; + } +} diff --git a/PlanTempus.Application/wwwroot/css/sidebar.css b/PlanTempus.Application/wwwroot/css/sidebar.css new file mode 100644 index 0000000..cc9354e --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/sidebar.css @@ -0,0 +1,246 @@ +/** + * Sidebar - Side Menu Component + * + * Navigation sidebar med collapse-funktionalitet + */ + +/* =========================================== + SIDE MENU CONTAINER + =========================================== */ +swp-side-menu { + grid-row: 1 / -1; + display: flex; + flex-direction: column; + background: var(--color-surface); + border-right: 1px solid var(--color-border); + overflow-y: auto; + overflow-x: hidden; +} + +/* =========================================== + HEADER + =========================================== */ +swp-side-menu-header { + display: flex; + align-items: center; + gap: 10px; + height: var(--topbar-height); + padding: 0 var(--spacing-4); + border-bottom: 1px solid var(--color-border); + flex-shrink: 0; +} + +swp-side-menu-header > i { + font-size: 26px; + color: var(--color-teal); +} + +swp-side-menu-logo { + font-size: var(--font-size-lg); + font-weight: 600; + color: var(--color-text); +} + +/* Toggle Button */ +swp-menu-toggle { + margin-left: auto; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + border-radius: var(--border-radius); + cursor: pointer; + color: var(--color-text-secondary); + transition: all var(--transition-fast); +} + +swp-menu-toggle:hover { + background: var(--color-background-hover); + color: var(--color-text); +} + +swp-menu-toggle i { + font-size: 18px; + color: inherit; + transition: transform var(--transition-normal); +} + +/* =========================================== + NAVIGATION + =========================================== */ +swp-side-menu-nav { + flex: 1; + padding: var(--spacing-3) 0; + overflow-y: auto; +} + +swp-side-menu-group { + display: block; + margin-bottom: var(--spacing-2); +} + +swp-side-menu-label { + display: block; + padding: var(--spacing-2) var(--spacing-4) 6px; + font-size: var(--font-size-xs); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); +} + +/* =========================================== + MENU ITEMS + =========================================== */ +swp-side-menu-item, +a[is="swp-side-menu-item"] { + display: flex; + align-items: center; + gap: var(--spacing-3); + padding: 10px var(--spacing-4); + color: var(--color-text); + cursor: pointer; + transition: all var(--transition-fast); + border-left: 3px solid transparent; + text-decoration: none; +} + +swp-side-menu-item:hover, +a[is="swp-side-menu-item"]:hover { + background: var(--color-background-hover); + text-decoration: none; +} + +swp-side-menu-item[data-active="true"], +a[is="swp-side-menu-item"][data-active="true"] { + background: var(--color-teal-light); + border-left-color: var(--color-teal); + color: var(--color-teal); + font-weight: 500; +} + +swp-side-menu-item i, +a[is="swp-side-menu-item"] i { + font-size: 20px; + flex-shrink: 0; +} + +/* =========================================== + FOOTER + =========================================== */ +swp-side-menu-footer { + display: flex; + flex-direction: column; + gap: var(--spacing-2); + padding: var(--spacing-3) var(--spacing-4); + border-top: 1px solid var(--color-border); + margin-top: auto; + flex-shrink: 0; +} + +swp-side-menu-action { + display: flex; + align-items: center; + justify-content: center; + gap: var(--spacing-2); + padding: 10px; + font-size: var(--font-size-md); + color: var(--color-text-secondary); + background: transparent; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + cursor: pointer; + transition: all var(--transition-fast); + font-family: var(--font-family); +} + +swp-side-menu-action:hover { + background: var(--color-background-hover); +} + +swp-side-menu-action.lock:hover { + color: var(--color-amber); + border-color: var(--color-amber); +} + +swp-side-menu-action.logout:hover { + color: var(--color-red); + border-color: var(--color-red); +} + +swp-side-menu-action i { + font-size: 18px; +} + +/* =========================================== + COLLAPSED STATE + =========================================== */ +swp-app-layout.menu-collapsed swp-side-menu { + overflow: visible; +} + +swp-app-layout.menu-collapsed swp-side-menu-logo, +swp-app-layout.menu-collapsed swp-side-menu-label, +swp-app-layout.menu-collapsed swp-side-menu-item span, +swp-app-layout.menu-collapsed swp-side-menu-action span, +swp-app-layout.menu-collapsed a[is="swp-side-menu-item"] span { + display: none; +} + +swp-app-layout.menu-collapsed swp-side-menu-header { + justify-content: center; + padding: 0; +} + +swp-app-layout.menu-collapsed swp-menu-toggle { + margin-left: 0; +} + +swp-app-layout.menu-collapsed swp-menu-toggle i { + transform: rotate(180deg); +} + +swp-app-layout.menu-collapsed swp-side-menu-item, +swp-app-layout.menu-collapsed a[is="swp-side-menu-item"] { + justify-content: center; + padding: var(--spacing-3); + border-left: none; +} + +swp-app-layout.menu-collapsed swp-side-menu-item[data-active="true"], +swp-app-layout.menu-collapsed a[is="swp-side-menu-item"][data-active="true"] { + border-left: none; + border-radius: var(--border-radius-lg); + margin: 0 var(--spacing-2); +} + +swp-app-layout.menu-collapsed swp-side-menu-action { + justify-content: center; + padding: var(--spacing-3); +} + +swp-app-layout.menu-collapsed swp-side-menu-footer { + padding: var(--spacing-3) var(--spacing-2); +} + +/* =========================================== + TOOLTIP (Collapsed State) + =========================================== */ +.swp-menu-tooltip { + position: fixed; + margin: 0; + padding: 6px var(--spacing-3); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + font-size: var(--font-size-md); + font-family: var(--font-family); + color: var(--color-text); + white-space: nowrap; + box-shadow: var(--shadow-md); + pointer-events: none; + z-index: var(--z-tooltip); +} diff --git a/PlanTempus.Application/wwwroot/css/stats.css b/PlanTempus.Application/wwwroot/css/stats.css new file mode 100644 index 0000000..2d50a5f --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/stats.css @@ -0,0 +1,258 @@ +/** + * Stats - Statistics Components + * + * Stat bars, cards, values og trends + */ + +/* =========================================== + STATS CONTAINER (Grid/Bar/Row) + =========================================== */ +swp-stats-bar, +swp-stats-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--spacing-4); + margin-bottom: var(--spacing-5); +} + +swp-stats-row { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--spacing-4); + margin-bottom: var(--spacing-5); +} + +/* =========================================== + STAT CARD + =========================================== */ +swp-stat-card { + display: flex; + flex-direction: column; + background: var(--color-surface); + border-radius: var(--border-radius-lg); + padding: var(--spacing-4) var(--spacing-5); + border: 1px solid var(--color-border); +} + +swp-stat-box { + display: flex; + flex-direction: column; + gap: var(--spacing-1); + padding: var(--spacing-4); + background: var(--color-surface); + border-radius: var(--border-radius); + border: 1px solid var(--color-border); +} + +/* =========================================== + STAT VALUE + =========================================== */ +swp-stat-value { + display: block; + font-size: 22px; + font-weight: 600; + font-family: var(--font-mono); + color: var(--color-text); + line-height: var(--line-height-tight); +} + +/* Larger variant for emphasis */ +swp-stat-card swp-stat-value, +swp-stat-box swp-stat-value { + font-size: 22px; + font-weight: 600; + font-family: var(--font-mono); + color: var(--color-text); +} + +/* =========================================== + STAT LABEL + =========================================== */ +swp-stat-label { + display: block; + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin-top: var(--spacing-1); +} + +swp-stat-box swp-stat-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--color-text-secondary); +} + +/* =========================================== + STAT SUBTITLE + =========================================== */ +swp-stat-subtitle { + display: block; + font-size: 11px; + color: var(--color-text-muted); + margin-top: var(--spacing-1); +} + +/* =========================================== + STAT TREND / CHANGE + =========================================== */ +swp-stat-trend, +swp-stat-change { + display: inline-flex; + align-items: center; + gap: var(--spacing-1); + font-size: var(--font-size-xs); + margin-top: var(--spacing-2); +} + +swp-stat-trend i, +swp-stat-change i { + font-size: 14px; +} + +/* Trend Up (positive) */ +swp-stat-trend.up, +swp-stat-change.positive { + color: var(--color-green); +} + +/* Trend Down (negative) */ +swp-stat-trend.down, +swp-stat-change.negative { + color: var(--color-red); +} + +/* Neutral trend */ +swp-stat-trend.neutral { + color: var(--color-text-secondary); +} + +/* =========================================== + COLOR MODIFIERS + =========================================== */ + +/* Highlight (Primary/Teal) */ +swp-stat-card.highlight swp-stat-value, +swp-stat-box.highlight swp-stat-value, +swp-stat-card.teal swp-stat-value { + color: var(--color-teal); +} + +/* Success (Green) */ +swp-stat-card.success swp-stat-value { + color: var(--color-green); +} + +/* Warning (Amber) */ +swp-stat-card.warning swp-stat-value, +swp-stat-card.amber swp-stat-value { + color: var(--color-amber); +} + +/* Danger (Red) */ +swp-stat-card.danger swp-stat-value, +swp-stat-card.negative swp-stat-value, +swp-stat-card.red swp-stat-value { + color: var(--color-red); +} + +/* Purple */ +swp-stat-card.purple swp-stat-value { + color: var(--color-purple); +} + +/* =========================================== + HIGHLIGHT CARD (Filled Background) + =========================================== */ +swp-stat-card.highlight.filled { + background: linear-gradient(135deg, var(--color-teal) 0%, #00695c 100%); + color: white; + border-color: transparent; +} + +swp-stat-card.highlight.filled swp-stat-value { + color: white; +} + +swp-stat-card.highlight.filled swp-stat-label { + color: rgba(255, 255, 255, 0.8); +} + +swp-stat-card.highlight.filled swp-stat-change { + color: rgba(255, 255, 255, 0.9); +} + +/* =========================================== + QUICK STATS (Compact Variant) + =========================================== */ +swp-quick-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--spacing-3); +} + +swp-quick-stat { + display: flex; + flex-direction: column; + text-align: center; + padding: var(--spacing-3); + background: var(--color-background); + border-radius: var(--border-radius); +} + +swp-quick-stat swp-stat-value { + font-size: 18px; +} + +swp-quick-stat swp-stat-label { + font-size: 11px; + margin-top: var(--spacing-1); +} + +/* =========================================== + STAT ITEM (Inline Variant) + =========================================== */ +swp-stat-item { + display: flex; + flex-direction: column; + gap: 2px; + padding: var(--spacing-2) var(--spacing-3); + background: var(--color-background); + border-radius: var(--border-radius); +} + +swp-stat-item swp-stat-value { + font-size: 16px; + font-weight: 600; +} + +swp-stat-item swp-stat-value.mono { + font-family: var(--font-mono); +} + +swp-stat-item swp-stat-label { + font-size: 11px; + margin-top: 0; +} + +/* =========================================== + RESPONSIVE + =========================================== */ +@media (max-width: 1200px) { + swp-stats-bar, + swp-stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 768px) { + swp-stats-bar, + swp-stats-grid, + swp-stats-row { + grid-template-columns: 1fr; + } + + swp-quick-stats { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/PlanTempus.Application/wwwroot/css/topbar.css b/PlanTempus.Application/wwwroot/css/topbar.css new file mode 100644 index 0000000..6c9d182 --- /dev/null +++ b/PlanTempus.Application/wwwroot/css/topbar.css @@ -0,0 +1,180 @@ +/** + * Topbar - App Header Bar + * + * Search, notifications og profil-menu + */ + +/* =========================================== + TOPBAR CONTAINER + =========================================== */ +swp-app-topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--spacing-5); + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + position: sticky; + top: 0; + z-index: var(--z-sticky); +} + +/* =========================================== + SEARCH + =========================================== */ +swp-topbar-search { + display: flex; + align-items: center; + gap: 10px; + padding: var(--spacing-2) var(--spacing-3); + background: var(--color-background); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + width: 320px; + transition: border-color var(--transition-fast); +} + +swp-topbar-search:focus-within { + border-color: var(--color-teal); +} + +swp-topbar-search i { + font-size: 18px; + color: var(--color-text-secondary); + flex-shrink: 0; +} + +swp-topbar-search input { + flex: 1; + border: none; + outline: none; + background: transparent; + font-size: var(--font-size-md); + font-family: var(--font-family); + color: var(--color-text); +} + +swp-topbar-search input::placeholder { + color: var(--color-text-secondary); +} + +swp-topbar-search kbd { + font-size: var(--font-size-xs); + font-family: var(--font-mono); + padding: 2px 6px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 4px; + color: var(--color-text-secondary); +} + +/* =========================================== + ACTIONS + =========================================== */ +swp-topbar-actions { + display: flex; + align-items: center; + gap: var(--spacing-2); +} + +/* Action Buttons */ +swp-topbar-btn { + display: flex; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + border: none; + background: transparent; + border-radius: var(--border-radius); + cursor: pointer; + transition: background var(--transition-fast); + position: relative; + color: var(--color-text-secondary); +} + +swp-topbar-btn:hover { + background: var(--color-background-hover); +} + +swp-topbar-btn i { + font-size: 22px; +} + +/* Notification Badge */ +swp-notification-badge { + position: absolute; + top: 6px; + right: 6px; + min-width: 16px; + height: 16px; + padding: 0 4px; + font-size: 10px; + font-weight: 600; + background: var(--color-red); + color: white; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Divider */ +swp-topbar-divider { + width: 1px; + height: 24px; + background: var(--color-border); + margin: 0 var(--spacing-2); +} + +/* =========================================== + PROFILE TRIGGER + =========================================== */ +swp-topbar-profile { + display: flex; + align-items: center; + gap: 10px; + padding: 6px var(--spacing-3) 6px 6px; + background: transparent; + border: 1px solid transparent; + border-radius: var(--border-radius); + cursor: pointer; + transition: all var(--transition-fast); +} + +swp-topbar-profile:hover { + background: var(--color-background-hover); + border-color: var(--color-border); +} + +swp-profile-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--color-teal); + color: white; + font-size: var(--font-size-sm); + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; +} + +swp-profile-info { + display: flex; + flex-direction: column; + align-items: flex-start; +} + +swp-profile-name { + font-size: var(--font-size-md); + font-weight: 500; + color: var(--color-text); + line-height: var(--line-height-tight); +} + +swp-profile-role { + font-size: var(--font-size-xs); + color: var(--color-text-secondary); + line-height: var(--line-height-tight); +} diff --git a/PlanTempus.Application/wwwroot/email-templates/verify-email.html b/PlanTempus.Application/wwwroot/email-templates/verify-email.html new file mode 100644 index 0000000..8290a3f --- /dev/null +++ b/PlanTempus.Application/wwwroot/email-templates/verify-email.html @@ -0,0 +1,308 @@ + + + + + + + Bekræft din email - Plan Tempus + + + + + + + + + + + + diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-Black.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-Black.woff new file mode 100644 index 0000000..65f3801 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-Black.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-BlackItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-BlackItalic.woff new file mode 100644 index 0000000..63eb02e Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-BlackItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-Bold.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-Bold.woff new file mode 100644 index 0000000..28fe6b6 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-Bold.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-BoldItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-BoldItalic.woff new file mode 100644 index 0000000..8ec438d Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-BoldItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraBold.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraBold.woff new file mode 100644 index 0000000..10df1ac Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraBold.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraBoldItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraBoldItalic.woff new file mode 100644 index 0000000..38090a0 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraBoldItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraLight.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraLight.woff new file mode 100644 index 0000000..8eb4095 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraLight.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraLightItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraLightItalic.woff new file mode 100644 index 0000000..560f65e Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-ExtraLightItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-Italic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-Italic.woff new file mode 100644 index 0000000..341cbb3 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-Italic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-Light.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-Light.woff new file mode 100644 index 0000000..61f6a5c Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-Light.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-LightItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-LightItalic.woff new file mode 100644 index 0000000..2706cd4 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-LightItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-Medium.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-Medium.woff new file mode 100644 index 0000000..e2b717d Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-Medium.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-MediumItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-MediumItalic.woff new file mode 100644 index 0000000..d8101a7 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-MediumItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-Regular.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-Regular.woff new file mode 100644 index 0000000..609eb3d Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-Regular.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-SemiBold.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-SemiBold.woff new file mode 100644 index 0000000..c097e14 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-SemiBold.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-SemiBoldItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-SemiBoldItalic.woff new file mode 100644 index 0000000..902dcfd Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-SemiBoldItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-Thin.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-Thin.woff new file mode 100644 index 0000000..d21fbd3 Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-Thin.woff differ diff --git a/PlanTempus.Application/wwwroot/fonts/Poppins-ThinItalic.woff b/PlanTempus.Application/wwwroot/fonts/Poppins-ThinItalic.woff new file mode 100644 index 0000000..ea3a42f Binary files /dev/null and b/PlanTempus.Application/wwwroot/fonts/Poppins-ThinItalic.woff differ diff --git a/PlanTempus.Application/wwwroot/images/poclogo.png b/PlanTempus.Application/wwwroot/images/poclogo.png new file mode 100644 index 0000000..d363158 Binary files /dev/null and b/PlanTempus.Application/wwwroot/images/poclogo.png differ diff --git a/PlanTempus.Application/wwwroot/js/app.js b/PlanTempus.Application/wwwroot/js/app.js new file mode 100644 index 0000000..701f2c5 --- /dev/null +++ b/PlanTempus.Application/wwwroot/js/app.js @@ -0,0 +1,604 @@ +var __defProp = Object.defineProperty; +var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); + +// wwwroot/ts/modules/sidebar.ts +var _SidebarController = class _SidebarController { + constructor() { + this.menuToggle = null; + this.appLayout = null; + this.menuTooltip = null; + this.menuToggle = document.getElementById("menuToggle"); + this.appLayout = document.querySelector("swp-app-layout"); + this.menuTooltip = document.getElementById("menuTooltip"); + this.setupListeners(); + this.setupTooltips(); + this.restoreState(); + } + /** + * Check if sidebar is collapsed + */ + get isCollapsed() { + return this.appLayout?.classList.contains("menu-collapsed") ?? false; + } + /** + * Toggle sidebar collapsed state + */ + toggle() { + if (!this.appLayout) + return; + this.appLayout.classList.toggle("menu-collapsed"); + localStorage.setItem("sidebar-collapsed", String(this.isCollapsed)); + } + /** + * Collapse the sidebar + */ + collapse() { + this.appLayout?.classList.add("menu-collapsed"); + localStorage.setItem("sidebar-collapsed", "true"); + } + /** + * Expand the sidebar + */ + expand() { + this.appLayout?.classList.remove("menu-collapsed"); + localStorage.setItem("sidebar-collapsed", "false"); + } + setupListeners() { + this.menuToggle?.addEventListener("click", () => this.toggle()); + } + setupTooltips() { + if (!this.menuTooltip) + return; + const menuItems = document.querySelectorAll("swp-side-menu-item[data-tooltip]"); + menuItems.forEach((item) => { + item.addEventListener("mouseenter", () => this.showTooltip(item)); + item.addEventListener("mouseleave", () => this.hideTooltip()); + }); + } + showTooltip(item) { + if (!this.isCollapsed || !this.menuTooltip) + return; + const rect = item.getBoundingClientRect(); + const tooltipText = item.dataset.tooltip; + if (!tooltipText) + return; + this.menuTooltip.textContent = tooltipText; + this.menuTooltip.style.left = `${rect.right + 8}px`; + this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`; + this.menuTooltip.style.transform = "translateY(-50%)"; + this.menuTooltip.showPopover(); + } + hideTooltip() { + this.menuTooltip?.hidePopover(); + } + restoreState() { + if (!this.appLayout) + return; + if (localStorage.getItem("sidebar-collapsed") === "true") { + this.appLayout.classList.add("menu-collapsed"); + } + } +}; +__name(_SidebarController, "SidebarController"); +var SidebarController = _SidebarController; + +// wwwroot/ts/modules/drawers.ts +var _DrawerController = class _DrawerController { + constructor() { + this.profileDrawer = null; + this.notificationDrawer = null; + this.todoDrawer = null; + this.newTodoDrawer = null; + this.overlay = null; + this.activeDrawer = null; + this.profileDrawer = document.getElementById("profileDrawer"); + this.notificationDrawer = document.getElementById("notificationDrawer"); + this.todoDrawer = document.getElementById("todoDrawer"); + this.newTodoDrawer = document.getElementById("newTodoDrawer"); + this.overlay = document.getElementById("drawerOverlay"); + this.setupListeners(); + } + /** + * Get currently active drawer name + */ + get active() { + return this.activeDrawer; + } + /** + * Open a drawer by name + */ + open(name) { + this.closeAll(); + const drawer = this.getDrawer(name); + if (drawer && this.overlay) { + drawer.classList.add("active"); + this.overlay.classList.add("active"); + document.body.style.overflow = "hidden"; + this.activeDrawer = name; + } + } + /** + * Close a specific drawer + */ + close(name) { + const drawer = this.getDrawer(name); + drawer?.classList.remove("active"); + if (this.overlay && !document.querySelector('.active[class*="drawer"]')) { + this.overlay.classList.remove("active"); + document.body.style.overflow = ""; + } + if (this.activeDrawer === name) { + this.activeDrawer = null; + } + } + /** + * Close all drawers + */ + closeAll() { + [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer].forEach((drawer) => drawer?.classList.remove("active")); + this.overlay?.classList.remove("active"); + document.body.style.overflow = ""; + this.activeDrawer = null; + } + /** + * Open profile drawer + */ + openProfile() { + this.open("profile"); + } + /** + * Open notification drawer + */ + openNotification() { + this.open("notification"); + } + /** + * Open todo drawer (slides on top of profile) + */ + openTodo() { + this.todoDrawer?.classList.add("active"); + } + /** + * Close todo drawer + */ + closeTodo() { + this.todoDrawer?.classList.remove("active"); + this.closeNewTodo(); + } + /** + * Open new todo drawer + */ + openNewTodo() { + this.newTodoDrawer?.classList.add("active"); + } + /** + * Close new todo drawer + */ + closeNewTodo() { + this.newTodoDrawer?.classList.remove("active"); + } + /** + * Mark all notifications as read + */ + markAllNotificationsRead() { + if (!this.notificationDrawer) + return; + const unreadItems = this.notificationDrawer.querySelectorAll( + 'swp-notification-item[data-unread="true"]' + ); + unreadItems.forEach((item) => item.removeAttribute("data-unread")); + const badge = document.querySelector("swp-notification-badge"); + if (badge) { + badge.style.display = "none"; + } + } + getDrawer(name) { + switch (name) { + case "profile": + return this.profileDrawer; + case "notification": + return this.notificationDrawer; + case "todo": + return this.todoDrawer; + case "newTodo": + return this.newTodoDrawer; + } + } + setupListeners() { + document.getElementById("profileTrigger")?.addEventListener("click", () => this.openProfile()); + document.getElementById("drawerClose")?.addEventListener("click", () => this.close("profile")); + document.getElementById("notificationsBtn")?.addEventListener("click", () => this.openNotification()); + document.getElementById("notificationDrawerClose")?.addEventListener("click", () => this.close("notification")); + document.getElementById("markAllRead")?.addEventListener("click", () => this.markAllNotificationsRead()); + document.getElementById("openTodoDrawer")?.addEventListener("click", () => this.openTodo()); + document.getElementById("todoDrawerBack")?.addEventListener("click", () => this.closeTodo()); + document.getElementById("addTodoBtn")?.addEventListener("click", () => this.openNewTodo()); + document.getElementById("newTodoDrawerBack")?.addEventListener("click", () => this.closeNewTodo()); + document.getElementById("cancelNewTodo")?.addEventListener("click", () => this.closeNewTodo()); + document.getElementById("saveNewTodo")?.addEventListener("click", () => this.closeNewTodo()); + this.overlay?.addEventListener("click", () => this.closeAll()); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") + this.closeAll(); + }); + this.todoDrawer?.addEventListener("click", (e) => this.handleTodoClick(e)); + document.addEventListener("click", (e) => this.handleVisibilityClick(e)); + } + handleTodoClick(e) { + const target = e.target; + const todoItem = target.closest("swp-todo-item"); + const checkbox = target.closest("swp-todo-checkbox"); + if (checkbox && todoItem) { + const isCompleted = todoItem.dataset.completed === "true"; + if (isCompleted) { + todoItem.removeAttribute("data-completed"); + } else { + todoItem.dataset.completed = "true"; + } + } + const sectionHeader = target.closest("swp-todo-section-header"); + if (sectionHeader) { + const section = sectionHeader.closest("swp-todo-section"); + section?.classList.toggle("collapsed"); + } + } + handleVisibilityClick(e) { + const target = e.target; + const option = target.closest("swp-visibility-option"); + if (option) { + document.querySelectorAll("swp-visibility-option").forEach((o) => o.classList.remove("active")); + option.classList.add("active"); + } + } +}; +__name(_DrawerController, "DrawerController"); +var DrawerController = _DrawerController; + +// wwwroot/ts/modules/theme.ts +var _ThemeController = class _ThemeController { + constructor() { + this.root = document.documentElement; + this.themeOptions = document.querySelectorAll("swp-theme-option"); + this.applyTheme(this.current); + this.updateUI(); + this.setupListeners(); + } + /** + * Get the current theme setting + */ + get current() { + const stored = localStorage.getItem(_ThemeController.STORAGE_KEY); + if (stored === "dark" || stored === "light" || stored === "system") { + return stored; + } + return "system"; + } + /** + * Check if dark mode is currently active + */ + get isDark() { + return this.root.classList.contains(_ThemeController.DARK_CLASS) || this.systemPrefersDark && !this.root.classList.contains(_ThemeController.LIGHT_CLASS); + } + /** + * Check if system prefers dark mode + */ + get systemPrefersDark() { + return window.matchMedia("(prefers-color-scheme: dark)").matches; + } + /** + * Set theme and persist preference + */ + set(theme) { + localStorage.setItem(_ThemeController.STORAGE_KEY, theme); + this.applyTheme(theme); + this.updateUI(); + } + /** + * Toggle between light and dark themes + */ + toggle() { + this.set(this.isDark ? "light" : "dark"); + } + applyTheme(theme) { + this.root.classList.remove(_ThemeController.DARK_CLASS, _ThemeController.LIGHT_CLASS); + if (theme === "dark") { + this.root.classList.add(_ThemeController.DARK_CLASS); + } else if (theme === "light") { + this.root.classList.add(_ThemeController.LIGHT_CLASS); + } + } + updateUI() { + if (!this.themeOptions) + return; + const darkActive = this.isDark; + this.themeOptions.forEach((option) => { + const theme = option.dataset.theme; + const isActive = theme === "dark" && darkActive || theme === "light" && !darkActive; + option.classList.toggle("active", isActive); + }); + } + setupListeners() { + this.themeOptions.forEach((option) => { + option.addEventListener("click", (e) => this.handleOptionClick(e)); + }); + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => this.handleSystemChange()); + } + handleOptionClick(e) { + const target = e.target; + const option = target.closest("swp-theme-option"); + if (option) { + const theme = option.dataset.theme; + if (theme) { + this.set(theme); + } + } + } + handleSystemChange() { + if (this.current === "system") { + this.updateUI(); + } + } +}; +__name(_ThemeController, "ThemeController"); +_ThemeController.STORAGE_KEY = "theme-preference"; +_ThemeController.DARK_CLASS = "dark-mode"; +_ThemeController.LIGHT_CLASS = "light-mode"; +var ThemeController = _ThemeController; + +// wwwroot/ts/modules/search.ts +var _SearchController = class _SearchController { + constructor() { + this.input = null; + this.container = null; + this.input = document.getElementById("globalSearch"); + this.container = document.querySelector("swp-topbar-search"); + this.setupListeners(); + } + /** + * Get current search value + */ + get value() { + return this.input?.value ?? ""; + } + /** + * Set search value + */ + set value(val) { + if (this.input) { + this.input.value = val; + } + } + /** + * Focus the search input + */ + focus() { + this.input?.focus(); + } + /** + * Blur the search input + */ + blur() { + this.input?.blur(); + } + /** + * Clear the search input + */ + clear() { + this.value = ""; + } + setupListeners() { + document.addEventListener("keydown", (e) => this.handleKeyboard(e)); + if (this.input) { + this.input.addEventListener("input", (e) => this.handleInput(e)); + const form = this.input.closest("form"); + form?.addEventListener("submit", (e) => this.handleSubmit(e)); + } + } + handleKeyboard(e) { + if ((e.metaKey || e.ctrlKey) && e.key === "k") { + e.preventDefault(); + this.focus(); + return; + } + if (e.key === "Escape" && document.activeElement === this.input) { + this.blur(); + } + } + handleInput(e) { + const target = e.target; + const query = target.value.trim(); + document.dispatchEvent(new CustomEvent("app:search", { + detail: { query }, + bubbles: true + })); + } + handleSubmit(e) { + e.preventDefault(); + const query = this.value.trim(); + if (!query) + return; + document.dispatchEvent(new CustomEvent("app:search-submit", { + detail: { query }, + bubbles: true + })); + } +}; +__name(_SearchController, "SearchController"); +var SearchController = _SearchController; + +// wwwroot/ts/modules/lockscreen.ts +var _LockScreenController = class _LockScreenController { + constructor(drawers) { + // Demo PIN + this.lockScreen = null; + this.pinInput = null; + this.pinKeypad = null; + this.lockTimeEl = null; + this.pinDigits = null; + this.currentPin = ""; + this.drawers = null; + this.drawers = drawers ?? null; + this.lockScreen = document.getElementById("lockScreen"); + this.pinInput = document.getElementById("pinInput"); + this.pinKeypad = document.getElementById("pinKeypad"); + this.lockTimeEl = document.getElementById("lockTime"); + this.pinDigits = this.pinInput?.querySelectorAll("swp-pin-digit") ?? null; + this.setupListeners(); + } + /** + * Check if lock screen is active + */ + get isActive() { + return this.lockScreen?.classList.contains("active") ?? false; + } + /** + * Show the lock screen + */ + show() { + this.drawers?.closeAll(); + if (this.lockScreen) { + this.lockScreen.classList.add("active"); + document.body.style.overflow = "hidden"; + } + this.currentPin = ""; + this.updateDisplay(); + if (this.lockTimeEl) { + this.lockTimeEl.textContent = `L\xE5st kl. ${this.formatTime()}`; + } + } + /** + * Hide the lock screen + */ + hide() { + if (this.lockScreen) { + this.lockScreen.classList.remove("active"); + document.body.style.overflow = ""; + } + this.currentPin = ""; + this.updateDisplay(); + } + formatTime() { + const now = /* @__PURE__ */ new Date(); + const hours = now.getHours().toString().padStart(2, "0"); + const minutes = now.getMinutes().toString().padStart(2, "0"); + return `${hours}:${minutes}`; + } + updateDisplay() { + if (!this.pinDigits) + return; + this.pinDigits.forEach((digit, index) => { + digit.classList.remove("filled", "error"); + if (index < this.currentPin.length) { + digit.textContent = "\u2022"; + digit.classList.add("filled"); + } else { + digit.textContent = ""; + } + }); + } + showError() { + if (!this.pinDigits) + return; + this.pinDigits.forEach((digit) => digit.classList.add("error")); + this.pinInput?.classList.add("shake"); + setTimeout(() => { + this.currentPin = ""; + this.updateDisplay(); + this.pinInput?.classList.remove("shake"); + }, 500); + } + verify() { + if (this.currentPin === _LockScreenController.CORRECT_PIN) { + this.hide(); + } else { + this.showError(); + } + } + addDigit(digit) { + if (this.currentPin.length >= 4) + return; + this.currentPin += digit; + this.updateDisplay(); + if (this.currentPin.length === 4) { + setTimeout(() => this.verify(), 200); + } + } + removeDigit() { + if (this.currentPin.length === 0) + return; + this.currentPin = this.currentPin.slice(0, -1); + this.updateDisplay(); + } + clearPin() { + this.currentPin = ""; + this.updateDisplay(); + } + setupListeners() { + this.pinKeypad?.addEventListener("click", (e) => this.handleKeypadClick(e)); + document.addEventListener("keydown", (e) => this.handleKeyboard(e)); + document.querySelector("swp-side-menu-action.lock")?.addEventListener("click", () => this.show()); + } + handleKeypadClick(e) { + const target = e.target; + const key = target.closest("swp-pin-key"); + if (!key) + return; + const digit = key.dataset.digit; + const action = key.dataset.action; + if (digit) { + this.addDigit(digit); + } else if (action === "backspace") { + this.removeDigit(); + } else if (action === "clear") { + this.clearPin(); + } + } + handleKeyboard(e) { + if (!this.isActive) + return; + e.preventDefault(); + if (e.key >= "0" && e.key <= "9") { + this.addDigit(e.key); + } else if (e.key === "Backspace") { + this.removeDigit(); + } else if (e.key === "Escape") { + this.clearPin(); + } + } +}; +__name(_LockScreenController, "LockScreenController"); +_LockScreenController.CORRECT_PIN = "1234"; +var LockScreenController = _LockScreenController; + +// wwwroot/ts/app.ts +var _App = class _App { + constructor() { + this.sidebar = new SidebarController(); + this.drawers = new DrawerController(); + this.theme = new ThemeController(); + this.search = new SearchController(); + this.lockScreen = new LockScreenController(this.drawers); + } +}; +__name(_App, "App"); +var App = _App; +var app; +function init() { + app = new App(); + if (typeof window !== "undefined") { + window.app = app; + } +} +__name(init, "init"); +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); +} else { + init(); +} +var app_default = App; +export { + App, + app, + app_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLi4vdHMvbW9kdWxlcy9zaWRlYmFyLnRzIiwgIi4uL3RzL21vZHVsZXMvZHJhd2Vycy50cyIsICIuLi90cy9tb2R1bGVzL3RoZW1lLnRzIiwgIi4uL3RzL21vZHVsZXMvc2VhcmNoLnRzIiwgIi4uL3RzL21vZHVsZXMvbG9ja3NjcmVlbi50cyIsICIuLi90cy9hcHAudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbIi8qKlxuICogU2lkZWJhciBDb250cm9sbGVyXG4gKlxuICogSGFuZGxlcyBzaWRlYmFyIGNvbGxhcHNlL2V4cGFuZCBhbmQgdG9vbHRpcCBmdW5jdGlvbmFsaXR5XG4gKi9cblxuZXhwb3J0IGNsYXNzIFNpZGViYXJDb250cm9sbGVyIHtcbiAgcHJpdmF0ZSBtZW51VG9nZ2xlOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIGFwcExheW91dDogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBtZW51VG9vbHRpcDogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcblxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICB0aGlzLm1lbnVUb2dnbGUgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbWVudVRvZ2dsZScpO1xuICAgIHRoaXMuYXBwTGF5b3V0ID0gZG9jdW1lbnQucXVlcnlTZWxlY3Rvcignc3dwLWFwcC1sYXlvdXQnKTtcbiAgICB0aGlzLm1lbnVUb29sdGlwID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ21lbnVUb29sdGlwJyk7XG5cbiAgICB0aGlzLnNldHVwTGlzdGVuZXJzKCk7XG4gICAgdGhpcy5zZXR1cFRvb2x0aXBzKCk7XG4gICAgdGhpcy5yZXN0b3JlU3RhdGUoKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDaGVjayBpZiBzaWRlYmFyIGlzIGNvbGxhcHNlZFxuICAgKi9cbiAgZ2V0IGlzQ29sbGFwc2VkKCk6IGJvb2xlYW4ge1xuICAgIHJldHVybiB0aGlzLmFwcExheW91dD8uY2xhc3NMaXN0LmNvbnRhaW5zKCdtZW51LWNvbGxhcHNlZCcpID8/IGZhbHNlO1xuICB9XG5cbiAgLyoqXG4gICAqIFRvZ2dsZSBzaWRlYmFyIGNvbGxhcHNlZCBzdGF0ZVxuICAgKi9cbiAgdG9nZ2xlKCk6IHZvaWQge1xuICAgIGlmICghdGhpcy5hcHBMYXlvdXQpIHJldHVybjtcblxuICAgIHRoaXMuYXBwTGF5b3V0LmNsYXNzTGlzdC50b2dnbGUoJ21lbnUtY29sbGFwc2VkJyk7XG4gICAgbG9jYWxTdG9yYWdlLnNldEl0ZW0oJ3NpZGViYXItY29sbGFwc2VkJywgU3RyaW5nKHRoaXMuaXNDb2xsYXBzZWQpKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDb2xsYXBzZSB0aGUgc2lkZWJhclxuICAgKi9cbiAgY29sbGFwc2UoKTogdm9pZCB7XG4gICAgdGhpcy5hcHBMYXlvdXQ/LmNsYXNzTGlzdC5hZGQoJ21lbnUtY29sbGFwc2VkJyk7XG4gICAgbG9jYWxTdG9yYWdlLnNldEl0ZW0oJ3NpZGViYXItY29sbGFwc2VkJywgJ3RydWUnKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBFeHBhbmQgdGhlIHNpZGViYXJcbiAgICovXG4gIGV4cGFuZCgpOiB2b2lkIHtcbiAgICB0aGlzLmFwcExheW91dD8uY2xhc3NMaXN0LnJlbW92ZSgnbWVudS1jb2xsYXBzZWQnKTtcbiAgICBsb2NhbFN0b3JhZ2Uuc2V0SXRlbSgnc2lkZWJhci1jb2xsYXBzZWQnLCAnZmFsc2UnKTtcbiAgfVxuXG4gIHByaXZhdGUgc2V0dXBMaXN0ZW5lcnMoKTogdm9pZCB7XG4gICAgdGhpcy5tZW51VG9nZ2xlPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMudG9nZ2xlKCkpO1xuICB9XG5cbiAgcHJpdmF0ZSBzZXR1cFRvb2x0aXBzKCk6IHZvaWQge1xuICAgIGlmICghdGhpcy5tZW51VG9vbHRpcCkgcmV0dXJuO1xuXG4gICAgY29uc3QgbWVudUl0ZW1zID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbDxIVE1MRWxlbWVudD4oJ3N3cC1zaWRlLW1lbnUtaXRlbVtkYXRhLXRvb2x0aXBdJyk7XG5cbiAgICBtZW51SXRlbXMuZm9yRWFjaChpdGVtID0+IHtcbiAgICAgIGl0ZW0uYWRkRXZlbnRMaXN0ZW5lcignbW91c2VlbnRlcicsICgpID0+IHRoaXMuc2hvd1Rvb2x0aXAoaXRlbSkpO1xuICAgICAgaXRlbS5hZGRFdmVudExpc3RlbmVyKCdtb3VzZWxlYXZlJywgKCkgPT4gdGhpcy5oaWRlVG9vbHRpcCgpKTtcbiAgICB9KTtcbiAgfVxuXG4gIHByaXZhdGUgc2hvd1Rvb2x0aXAoaXRlbTogSFRNTEVsZW1lbnQpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMuaXNDb2xsYXBzZWQgfHwgIXRoaXMubWVudVRvb2x0aXApIHJldHVybjtcblxuICAgIGNvbnN0IHJlY3QgPSBpdGVtLmdldEJvdW5kaW5nQ2xpZW50UmVjdCgpO1xuICAgIGNvbnN0IHRvb2x0aXBUZXh0ID0gaXRlbS5kYXRhc2V0LnRvb2x0aXA7XG5cbiAgICBpZiAoIXRvb2x0aXBUZXh0KSByZXR1cm47XG5cbiAgICB0aGlzLm1lbnVUb29sdGlwLnRleHRDb250ZW50ID0gdG9vbHRpcFRleHQ7XG4gICAgdGhpcy5tZW51VG9vbHRpcC5zdHlsZS5sZWZ0ID0gYCR7cmVjdC5yaWdodCArIDh9cHhgO1xuICAgIHRoaXMubWVudVRvb2x0aXAuc3R5bGUudG9wID0gYCR7cmVjdC50b3AgKyByZWN0LmhlaWdodCAvIDJ9cHhgO1xuICAgIHRoaXMubWVudVRvb2x0aXAuc3R5bGUudHJhbnNmb3JtID0gJ3RyYW5zbGF0ZVkoLTUwJSknO1xuICAgIHRoaXMubWVudVRvb2x0aXAuc2hvd1BvcG92ZXIoKTtcbiAgfVxuXG4gIHByaXZhdGUgaGlkZVRvb2x0aXAoKTogdm9pZCB7XG4gICAgdGhpcy5tZW51VG9vbHRpcD8uaGlkZVBvcG92ZXIoKTtcbiAgfVxuXG4gIHByaXZhdGUgcmVzdG9yZVN0YXRlKCk6IHZvaWQge1xuICAgIGlmICghdGhpcy5hcHBMYXlvdXQpIHJldHVybjtcblxuICAgIGlmIChsb2NhbFN0b3JhZ2UuZ2V0SXRlbSgnc2lkZWJhci1jb2xsYXBzZWQnKSA9PT0gJ3RydWUnKSB7XG4gICAgICB0aGlzLmFwcExheW91dC5jbGFzc0xpc3QuYWRkKCdtZW51LWNvbGxhcHNlZCcpO1xuICAgIH1cbiAgfVxufVxuIiwgIi8qKlxuICogRHJhd2VyIENvbnRyb2xsZXJcbiAqXG4gKiBIYW5kbGVzIGFsbCBkcmF3ZXIgZnVuY3Rpb25hbGl0eSBpbmNsdWRpbmcgcHJvZmlsZSwgbm90aWZpY2F0aW9ucywgYW5kIHRvZG8gZHJhd2Vyc1xuICovXG5cbmV4cG9ydCB0eXBlIERyYXdlck5hbWUgPSAncHJvZmlsZScgfCAnbm90aWZpY2F0aW9uJyB8ICd0b2RvJyB8ICduZXdUb2RvJztcblxuZXhwb3J0IGNsYXNzIERyYXdlckNvbnRyb2xsZXIge1xuICBwcml2YXRlIHByb2ZpbGVEcmF3ZXI6IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XG4gIHByaXZhdGUgbm90aWZpY2F0aW9uRHJhd2VyOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIHRvZG9EcmF3ZXI6IEhUTUxFbGVtZW50IHwgbnVsbCA9IG51bGw7XG4gIHByaXZhdGUgbmV3VG9kb0RyYXdlcjogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBvdmVybGF5OiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIGFjdGl2ZURyYXdlcjogRHJhd2VyTmFtZSB8IG51bGwgPSBudWxsO1xuXG4gIGNvbnN0cnVjdG9yKCkge1xuICAgIHRoaXMucHJvZmlsZURyYXdlciA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwcm9maWxlRHJhd2VyJyk7XG4gICAgdGhpcy5ub3RpZmljYXRpb25EcmF3ZXIgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbm90aWZpY2F0aW9uRHJhd2VyJyk7XG4gICAgdGhpcy50b2RvRHJhd2VyID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3RvZG9EcmF3ZXInKTtcbiAgICB0aGlzLm5ld1RvZG9EcmF3ZXIgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbmV3VG9kb0RyYXdlcicpO1xuICAgIHRoaXMub3ZlcmxheSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdkcmF3ZXJPdmVybGF5Jyk7XG5cbiAgICB0aGlzLnNldHVwTGlzdGVuZXJzKCk7XG4gIH1cblxuICAvKipcbiAgICogR2V0IGN1cnJlbnRseSBhY3RpdmUgZHJhd2VyIG5hbWVcbiAgICovXG4gIGdldCBhY3RpdmUoKTogRHJhd2VyTmFtZSB8IG51bGwge1xuICAgIHJldHVybiB0aGlzLmFjdGl2ZURyYXdlcjtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIGEgZHJhd2VyIGJ5IG5hbWVcbiAgICovXG4gIG9wZW4obmFtZTogRHJhd2VyTmFtZSk6IHZvaWQge1xuICAgIHRoaXMuY2xvc2VBbGwoKTtcblxuICAgIGNvbnN0IGRyYXdlciA9IHRoaXMuZ2V0RHJhd2VyKG5hbWUpO1xuICAgIGlmIChkcmF3ZXIgJiYgdGhpcy5vdmVybGF5KSB7XG4gICAgICBkcmF3ZXIuY2xhc3NMaXN0LmFkZCgnYWN0aXZlJyk7XG4gICAgICB0aGlzLm92ZXJsYXkuY2xhc3NMaXN0LmFkZCgnYWN0aXZlJyk7XG4gICAgICBkb2N1bWVudC5ib2R5LnN0eWxlLm92ZXJmbG93ID0gJ2hpZGRlbic7XG4gICAgICB0aGlzLmFjdGl2ZURyYXdlciA9IG5hbWU7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIENsb3NlIGEgc3BlY2lmaWMgZHJhd2VyXG4gICAqL1xuICBjbG9zZShuYW1lOiBEcmF3ZXJOYW1lKTogdm9pZCB7XG4gICAgY29uc3QgZHJhd2VyID0gdGhpcy5nZXREcmF3ZXIobmFtZSk7XG4gICAgZHJhd2VyPy5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcblxuICAgIC8vIE9ubHkgaGlkZSBvdmVybGF5IGlmIG5vIGRyYXdlcnMgYXJlIGFjdGl2ZVxuICAgIGlmICh0aGlzLm92ZXJsYXkgJiYgIWRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoJy5hY3RpdmVbY2xhc3MqPVwiZHJhd2VyXCJdJykpIHtcbiAgICAgIHRoaXMub3ZlcmxheS5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICAgIGRvY3VtZW50LmJvZHkuc3R5bGUub3ZlcmZsb3cgPSAnJztcbiAgICB9XG5cbiAgICBpZiAodGhpcy5hY3RpdmVEcmF3ZXIgPT09IG5hbWUpIHtcbiAgICAgIHRoaXMuYWN0aXZlRHJhd2VyID0gbnVsbDtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQ2xvc2UgYWxsIGRyYXdlcnNcbiAgICovXG4gIGNsb3NlQWxsKCk6IHZvaWQge1xuICAgIFt0aGlzLnByb2ZpbGVEcmF3ZXIsIHRoaXMubm90aWZpY2F0aW9uRHJhd2VyLCB0aGlzLnRvZG9EcmF3ZXIsIHRoaXMubmV3VG9kb0RyYXdlcl1cbiAgICAgIC5mb3JFYWNoKGRyYXdlciA9PiBkcmF3ZXI/LmNsYXNzTGlzdC5yZW1vdmUoJ2FjdGl2ZScpKTtcblxuICAgIHRoaXMub3ZlcmxheT8uY2xhc3NMaXN0LnJlbW92ZSgnYWN0aXZlJyk7XG4gICAgZG9jdW1lbnQuYm9keS5zdHlsZS5vdmVyZmxvdyA9ICcnO1xuICAgIHRoaXMuYWN0aXZlRHJhd2VyID0gbnVsbDtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIHByb2ZpbGUgZHJhd2VyXG4gICAqL1xuICBvcGVuUHJvZmlsZSgpOiB2b2lkIHtcbiAgICB0aGlzLm9wZW4oJ3Byb2ZpbGUnKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIG5vdGlmaWNhdGlvbiBkcmF3ZXJcbiAgICovXG4gIG9wZW5Ob3RpZmljYXRpb24oKTogdm9pZCB7XG4gICAgdGhpcy5vcGVuKCdub3RpZmljYXRpb24nKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBPcGVuIHRvZG8gZHJhd2VyIChzbGlkZXMgb24gdG9wIG9mIHByb2ZpbGUpXG4gICAqL1xuICBvcGVuVG9kbygpOiB2b2lkIHtcbiAgICB0aGlzLnRvZG9EcmF3ZXI/LmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICB9XG5cbiAgLyoqXG4gICAqIENsb3NlIHRvZG8gZHJhd2VyXG4gICAqL1xuICBjbG9zZVRvZG8oKTogdm9pZCB7XG4gICAgdGhpcy50b2RvRHJhd2VyPy5jbGFzc0xpc3QucmVtb3ZlKCdhY3RpdmUnKTtcbiAgICB0aGlzLmNsb3NlTmV3VG9kbygpO1xuICB9XG5cbiAgLyoqXG4gICAqIE9wZW4gbmV3IHRvZG8gZHJhd2VyXG4gICAqL1xuICBvcGVuTmV3VG9kbygpOiB2b2lkIHtcbiAgICB0aGlzLm5ld1RvZG9EcmF3ZXI/LmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICB9XG5cbiAgLyoqXG4gICAqIENsb3NlIG5ldyB0b2RvIGRyYXdlclxuICAgKi9cbiAgY2xvc2VOZXdUb2RvKCk6IHZvaWQge1xuICAgIHRoaXMubmV3VG9kb0RyYXdlcj8uY2xhc3NMaXN0LnJlbW92ZSgnYWN0aXZlJyk7XG4gIH1cblxuICAvKipcbiAgICogTWFyayBhbGwgbm90aWZpY2F0aW9ucyBhcyByZWFkXG4gICAqL1xuICBtYXJrQWxsTm90aWZpY2F0aW9uc1JlYWQoKTogdm9pZCB7XG4gICAgaWYgKCF0aGlzLm5vdGlmaWNhdGlvbkRyYXdlcikgcmV0dXJuO1xuXG4gICAgY29uc3QgdW5yZWFkSXRlbXMgPSB0aGlzLm5vdGlmaWNhdGlvbkRyYXdlci5xdWVyeVNlbGVjdG9yQWxsPEhUTUxFbGVtZW50PihcbiAgICAgICdzd3Atbm90aWZpY2F0aW9uLWl0ZW1bZGF0YS11bnJlYWQ9XCJ0cnVlXCJdJ1xuICAgICk7XG4gICAgdW5yZWFkSXRlbXMuZm9yRWFjaChpdGVtID0+IGl0ZW0ucmVtb3ZlQXR0cmlidXRlKCdkYXRhLXVucmVhZCcpKTtcblxuICAgIGNvbnN0IGJhZGdlID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcjxIVE1MRWxlbWVudD4oJ3N3cC1ub3RpZmljYXRpb24tYmFkZ2UnKTtcbiAgICBpZiAoYmFkZ2UpIHtcbiAgICAgIGJhZGdlLnN0eWxlLmRpc3BsYXkgPSAnbm9uZSc7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBnZXREcmF3ZXIobmFtZTogRHJhd2VyTmFtZSk6IEhUTUxFbGVtZW50IHwgbnVsbCB7XG4gICAgc3dpdGNoIChuYW1lKSB7XG4gICAgICBjYXNlICdwcm9maWxlJzogcmV0dXJuIHRoaXMucHJvZmlsZURyYXdlcjtcbiAgICAgIGNhc2UgJ25vdGlmaWNhdGlvbic6IHJldHVybiB0aGlzLm5vdGlmaWNhdGlvbkRyYXdlcjtcbiAgICAgIGNhc2UgJ3RvZG8nOiByZXR1cm4gdGhpcy50b2RvRHJhd2VyO1xuICAgICAgY2FzZSAnbmV3VG9kbyc6IHJldHVybiB0aGlzLm5ld1RvZG9EcmF3ZXI7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBzZXR1cExpc3RlbmVycygpOiB2b2lkIHtcbiAgICAvLyBQcm9maWxlIGRyYXdlciB0cmlnZ2Vyc1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwcm9maWxlVHJpZ2dlcicpXG4gICAgICA/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5vcGVuUHJvZmlsZSgpKTtcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZHJhd2VyQ2xvc2UnKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMuY2xvc2UoJ3Byb2ZpbGUnKSk7XG5cbiAgICAvLyBOb3RpZmljYXRpb24gZHJhd2VyIHRyaWdnZXJzXG4gICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ25vdGlmaWNhdGlvbnNCdG4nKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMub3Blbk5vdGlmaWNhdGlvbigpKTtcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbm90aWZpY2F0aW9uRHJhd2VyQ2xvc2UnKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMuY2xvc2UoJ25vdGlmaWNhdGlvbicpKTtcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbWFya0FsbFJlYWQnKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMubWFya0FsbE5vdGlmaWNhdGlvbnNSZWFkKCkpO1xuXG4gICAgLy8gVG9kbyBkcmF3ZXIgdHJpZ2dlcnNcbiAgICBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnb3BlblRvZG9EcmF3ZXInKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMub3BlblRvZG8oKSk7XG4gICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3RvZG9EcmF3ZXJCYWNrJylcbiAgICAgID8uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB0aGlzLmNsb3NlVG9kbygpKTtcblxuICAgIC8vIE5ldyB0b2RvIGRyYXdlciB0cmlnZ2Vyc1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhZGRUb2RvQnRuJylcbiAgICAgID8uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiB0aGlzLm9wZW5OZXdUb2RvKCkpO1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCduZXdUb2RvRHJhd2VyQmFjaycpXG4gICAgICA/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5jbG9zZU5ld1RvZG8oKSk7XG4gICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2NhbmNlbE5ld1RvZG8nKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMuY2xvc2VOZXdUb2RvKCkpO1xuICAgIGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdzYXZlTmV3VG9kbycpXG4gICAgICA/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5jbG9zZU5ld1RvZG8oKSk7XG5cbiAgICAvLyBPdmVybGF5IGNsaWNrIGNsb3NlcyBhbGxcbiAgICB0aGlzLm92ZXJsYXk/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gdGhpcy5jbG9zZUFsbCgpKTtcblxuICAgIC8vIEVzY2FwZSBrZXkgY2xvc2VzIGFsbFxuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLCAoZTogS2V5Ym9hcmRFdmVudCkgPT4ge1xuICAgICAgaWYgKGUua2V5ID09PSAnRXNjYXBlJykgdGhpcy5jbG9zZUFsbCgpO1xuICAgIH0pO1xuXG4gICAgLy8gVG9kbyBpbnRlcmFjdGlvbnNcbiAgICB0aGlzLnRvZG9EcmF3ZXI/LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGUpID0+IHRoaXMuaGFuZGxlVG9kb0NsaWNrKGUpKTtcblxuICAgIC8vIFZpc2liaWxpdHkgb3B0aW9uc1xuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGUpID0+IHRoaXMuaGFuZGxlVmlzaWJpbGl0eUNsaWNrKGUpKTtcbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlVG9kb0NsaWNrKGU6IEV2ZW50KTogdm9pZCB7XG4gICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7XG4gICAgY29uc3QgdG9kb0l0ZW0gPSB0YXJnZXQuY2xvc2VzdDxIVE1MRWxlbWVudD4oJ3N3cC10b2RvLWl0ZW0nKTtcbiAgICBjb25zdCBjaGVja2JveCA9IHRhcmdldC5jbG9zZXN0PEhUTUxFbGVtZW50Pignc3dwLXRvZG8tY2hlY2tib3gnKTtcblxuICAgIGlmIChjaGVja2JveCAmJiB0b2RvSXRlbSkge1xuICAgICAgY29uc3QgaXNDb21wbGV0ZWQgPSB0b2RvSXRlbS5kYXRhc2V0LmNvbXBsZXRlZCA9PT0gJ3RydWUnO1xuICAgICAgaWYgKGlzQ29tcGxldGVkKSB7XG4gICAgICAgIHRvZG9JdGVtLnJlbW92ZUF0dHJpYnV0ZSgnZGF0YS1jb21wbGV0ZWQnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHRvZG9JdGVtLmRhdGFzZXQuY29tcGxldGVkID0gJ3RydWUnO1xuICAgICAgfVxuICAgIH1cblxuICAgIC8vIFRvZ2dsZSBzZWN0aW9uIGNvbGxhcHNlXG4gICAgY29uc3Qgc2VjdGlvbkhlYWRlciA9IHRhcmdldC5jbG9zZXN0PEhUTUxFbGVtZW50Pignc3dwLXRvZG8tc2VjdGlvbi1oZWFkZXInKTtcbiAgICBpZiAoc2VjdGlvbkhlYWRlcikge1xuICAgICAgY29uc3Qgc2VjdGlvbiA9IHNlY3Rpb25IZWFkZXIuY2xvc2VzdDxIVE1MRWxlbWVudD4oJ3N3cC10b2RvLXNlY3Rpb24nKTtcbiAgICAgIHNlY3Rpb24/LmNsYXNzTGlzdC50b2dnbGUoJ2NvbGxhcHNlZCcpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlVmlzaWJpbGl0eUNsaWNrKGU6IEV2ZW50KTogdm9pZCB7XG4gICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7XG4gICAgY29uc3Qgb3B0aW9uID0gdGFyZ2V0LmNsb3Nlc3Q8SFRNTEVsZW1lbnQ+KCdzd3AtdmlzaWJpbGl0eS1vcHRpb24nKTtcblxuICAgIGlmIChvcHRpb24pIHtcbiAgICAgIGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGw8SFRNTEVsZW1lbnQ+KCdzd3AtdmlzaWJpbGl0eS1vcHRpb24nKVxuICAgICAgICAuZm9yRWFjaChvID0+IG8uY2xhc3NMaXN0LnJlbW92ZSgnYWN0aXZlJykpO1xuICAgICAgb3B0aW9uLmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICAgIH1cbiAgfVxufVxuIiwgIi8qKlxuICogVGhlbWUgQ29udHJvbGxlclxuICpcbiAqIEhhbmRsZXMgZGFyay9saWdodCBtb2RlIHN3aXRjaGluZyBhbmQgc3lzdGVtIHByZWZlcmVuY2UgZGV0ZWN0aW9uXG4gKi9cblxuZXhwb3J0IHR5cGUgVGhlbWUgPSAnbGlnaHQnIHwgJ2RhcmsnIHwgJ3N5c3RlbSc7XG5cbmV4cG9ydCBjbGFzcyBUaGVtZUNvbnRyb2xsZXIge1xuICBwcml2YXRlIHN0YXRpYyByZWFkb25seSBTVE9SQUdFX0tFWSA9ICd0aGVtZS1wcmVmZXJlbmNlJztcbiAgcHJpdmF0ZSBzdGF0aWMgcmVhZG9ubHkgREFSS19DTEFTUyA9ICdkYXJrLW1vZGUnO1xuICBwcml2YXRlIHN0YXRpYyByZWFkb25seSBMSUdIVF9DTEFTUyA9ICdsaWdodC1tb2RlJztcblxuICBwcml2YXRlIHJvb3Q6IEhUTUxFbGVtZW50O1xuICBwcml2YXRlIHRoZW1lT3B0aW9uczogTm9kZUxpc3RPZjxIVE1MRWxlbWVudD47XG5cbiAgY29uc3RydWN0b3IoKSB7XG4gICAgdGhpcy5yb290ID0gZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50O1xuICAgIHRoaXMudGhlbWVPcHRpb25zID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvckFsbDxIVE1MRWxlbWVudD4oJ3N3cC10aGVtZS1vcHRpb24nKTtcblxuICAgIHRoaXMuYXBwbHlUaGVtZSh0aGlzLmN1cnJlbnQpO1xuICAgIHRoaXMudXBkYXRlVUkoKTtcbiAgICB0aGlzLnNldHVwTGlzdGVuZXJzKCk7XG4gIH1cblxuICAvKipcbiAgICogR2V0IHRoZSBjdXJyZW50IHRoZW1lIHNldHRpbmdcbiAgICovXG4gIGdldCBjdXJyZW50KCk6IFRoZW1lIHtcbiAgICBjb25zdCBzdG9yZWQgPSBsb2NhbFN0b3JhZ2UuZ2V0SXRlbShUaGVtZUNvbnRyb2xsZXIuU1RPUkFHRV9LRVkpIGFzIFRoZW1lIHwgbnVsbDtcbiAgICBpZiAoc3RvcmVkID09PSAnZGFyaycgfHwgc3RvcmVkID09PSAnbGlnaHQnIHx8IHN0b3JlZCA9PT0gJ3N5c3RlbScpIHtcbiAgICAgIHJldHVybiBzdG9yZWQ7XG4gICAgfVxuICAgIHJldHVybiAnc3lzdGVtJztcbiAgfVxuXG4gIC8qKlxuICAgKiBDaGVjayBpZiBkYXJrIG1vZGUgaXMgY3VycmVudGx5IGFjdGl2ZVxuICAgKi9cbiAgZ2V0IGlzRGFyaygpOiBib29sZWFuIHtcbiAgICByZXR1cm4gdGhpcy5yb290LmNsYXNzTGlzdC5jb250YWlucyhUaGVtZUNvbnRyb2xsZXIuREFSS19DTEFTUykgfHxcbiAgICAgICh0aGlzLnN5c3RlbVByZWZlcnNEYXJrICYmICF0aGlzLnJvb3QuY2xhc3NMaXN0LmNvbnRhaW5zKFRoZW1lQ29udHJvbGxlci5MSUdIVF9DTEFTUykpO1xuICB9XG5cbiAgLyoqXG4gICAqIENoZWNrIGlmIHN5c3RlbSBwcmVmZXJzIGRhcmsgbW9kZVxuICAgKi9cbiAgZ2V0IHN5c3RlbVByZWZlcnNEYXJrKCk6IGJvb2xlYW4ge1xuICAgIHJldHVybiB3aW5kb3cubWF0Y2hNZWRpYSgnKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKScpLm1hdGNoZXM7XG4gIH1cblxuICAvKipcbiAgICogU2V0IHRoZW1lIGFuZCBwZXJzaXN0IHByZWZlcmVuY2VcbiAgICovXG4gIHNldCh0aGVtZTogVGhlbWUpOiB2b2lkIHtcbiAgICBsb2NhbFN0b3JhZ2Uuc2V0SXRlbShUaGVtZUNvbnRyb2xsZXIuU1RPUkFHRV9LRVksIHRoZW1lKTtcbiAgICB0aGlzLmFwcGx5VGhlbWUodGhlbWUpO1xuICAgIHRoaXMudXBkYXRlVUkoKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBUb2dnbGUgYmV0d2VlbiBsaWdodCBhbmQgZGFyayB0aGVtZXNcbiAgICovXG4gIHRvZ2dsZSgpOiB2b2lkIHtcbiAgICB0aGlzLnNldCh0aGlzLmlzRGFyayA/ICdsaWdodCcgOiAnZGFyaycpO1xuICB9XG5cbiAgcHJpdmF0ZSBhcHBseVRoZW1lKHRoZW1lOiBUaGVtZSk6IHZvaWQge1xuICAgIHRoaXMucm9vdC5jbGFzc0xpc3QucmVtb3ZlKFRoZW1lQ29udHJvbGxlci5EQVJLX0NMQVNTLCBUaGVtZUNvbnRyb2xsZXIuTElHSFRfQ0xBU1MpO1xuXG4gICAgaWYgKHRoZW1lID09PSAnZGFyaycpIHtcbiAgICAgIHRoaXMucm9vdC5jbGFzc0xpc3QuYWRkKFRoZW1lQ29udHJvbGxlci5EQVJLX0NMQVNTKTtcbiAgICB9IGVsc2UgaWYgKHRoZW1lID09PSAnbGlnaHQnKSB7XG4gICAgICB0aGlzLnJvb3QuY2xhc3NMaXN0LmFkZChUaGVtZUNvbnRyb2xsZXIuTElHSFRfQ0xBU1MpO1xuICAgIH1cbiAgICAvLyAnc3lzdGVtJyBsZWF2ZXMgYm90aCBjbGFzc2VzIG9mZiwgbGV0dGluZyBDU1MgbWVkaWEgcXVlcnkgaGFuZGxlIGl0XG4gIH1cblxuICBwcml2YXRlIHVwZGF0ZVVJKCk6IHZvaWQge1xuICAgIGlmICghdGhpcy50aGVtZU9wdGlvbnMpIHJldHVybjtcblxuICAgIGNvbnN0IGRhcmtBY3RpdmUgPSB0aGlzLmlzRGFyaztcblxuICAgIHRoaXMudGhlbWVPcHRpb25zLmZvckVhY2gob3B0aW9uID0+IHtcbiAgICAgIGNvbnN0IHRoZW1lID0gb3B0aW9uLmRhdGFzZXQudGhlbWUgYXMgVGhlbWU7XG4gICAgICBjb25zdCBpc0FjdGl2ZSA9ICh0aGVtZSA9PT0gJ2RhcmsnICYmIGRhcmtBY3RpdmUpIHx8ICh0aGVtZSA9PT0gJ2xpZ2h0JyAmJiAhZGFya0FjdGl2ZSk7XG4gICAgICBvcHRpb24uY2xhc3NMaXN0LnRvZ2dsZSgnYWN0aXZlJywgaXNBY3RpdmUpO1xuICAgIH0pO1xuICB9XG5cbiAgcHJpdmF0ZSBzZXR1cExpc3RlbmVycygpOiB2b2lkIHtcbiAgICAvLyBUaGVtZSBvcHRpb24gY2xpY2tzXG4gICAgdGhpcy50aGVtZU9wdGlvbnMuZm9yRWFjaChvcHRpb24gPT4ge1xuICAgICAgb3B0aW9uLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKGUpID0+IHRoaXMuaGFuZGxlT3B0aW9uQ2xpY2soZSkpO1xuICAgIH0pO1xuXG4gICAgLy8gU3lzdGVtIHRoZW1lIGNoYW5nZXNcbiAgICB3aW5kb3cubWF0Y2hNZWRpYSgnKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKScpXG4gICAgICAuYWRkRXZlbnRMaXN0ZW5lcignY2hhbmdlJywgKCkgPT4gdGhpcy5oYW5kbGVTeXN0ZW1DaGFuZ2UoKSk7XG4gIH1cblxuICBwcml2YXRlIGhhbmRsZU9wdGlvbkNsaWNrKGU6IEV2ZW50KTogdm9pZCB7XG4gICAgY29uc3QgdGFyZ2V0ID0gZS50YXJnZXQgYXMgSFRNTEVsZW1lbnQ7XG4gICAgY29uc3Qgb3B0aW9uID0gdGFyZ2V0LmNsb3Nlc3Q8SFRNTEVsZW1lbnQ+KCdzd3AtdGhlbWUtb3B0aW9uJyk7XG5cbiAgICBpZiAob3B0aW9uKSB7XG4gICAgICBjb25zdCB0aGVtZSA9IG9wdGlvbi5kYXRhc2V0LnRoZW1lIGFzIFRoZW1lO1xuICAgICAgaWYgKHRoZW1lKSB7XG4gICAgICAgIHRoaXMuc2V0KHRoZW1lKTtcbiAgICAgIH1cbiAgICB9XG4gIH1cblxuICBwcml2YXRlIGhhbmRsZVN5c3RlbUNoYW5nZSgpOiB2b2lkIHtcbiAgICAvLyBPbmx5IHJlYWN0IHRvIHN5c3RlbSBjaGFuZ2VzIGlmIHdlJ3JlIHVzaW5nIHN5c3RlbSBwcmVmZXJlbmNlXG4gICAgaWYgKHRoaXMuY3VycmVudCA9PT0gJ3N5c3RlbScpIHtcbiAgICAgIHRoaXMudXBkYXRlVUkoKTtcbiAgICB9XG4gIH1cbn1cbiIsICIvKipcbiAqIFNlYXJjaCBDb250cm9sbGVyXG4gKlxuICogSGFuZGxlcyBnbG9iYWwgc2VhcmNoIGZ1bmN0aW9uYWxpdHkgYW5kIGtleWJvYXJkIHNob3J0Y3V0c1xuICovXG5cbmV4cG9ydCBjbGFzcyBTZWFyY2hDb250cm9sbGVyIHtcbiAgcHJpdmF0ZSBpbnB1dDogSFRNTElucHV0RWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIGNvbnRhaW5lcjogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcblxuICBjb25zdHJ1Y3RvcigpIHtcbiAgICB0aGlzLmlucHV0ID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ2dsb2JhbFNlYXJjaCcpIGFzIEhUTUxJbnB1dEVsZW1lbnQgfCBudWxsO1xuICAgIHRoaXMuY29udGFpbmVyID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcjxIVE1MRWxlbWVudD4oJ3N3cC10b3BiYXItc2VhcmNoJyk7XG5cbiAgICB0aGlzLnNldHVwTGlzdGVuZXJzKCk7XG4gIH1cblxuICAvKipcbiAgICogR2V0IGN1cnJlbnQgc2VhcmNoIHZhbHVlXG4gICAqL1xuICBnZXQgdmFsdWUoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gdGhpcy5pbnB1dD8udmFsdWUgPz8gJyc7XG4gIH1cblxuICAvKipcbiAgICogU2V0IHNlYXJjaCB2YWx1ZVxuICAgKi9cbiAgc2V0IHZhbHVlKHZhbDogc3RyaW5nKSB7XG4gICAgaWYgKHRoaXMuaW5wdXQpIHtcbiAgICAgIHRoaXMuaW5wdXQudmFsdWUgPSB2YWw7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIEZvY3VzIHRoZSBzZWFyY2ggaW5wdXRcbiAgICovXG4gIGZvY3VzKCk6IHZvaWQge1xuICAgIHRoaXMuaW5wdXQ/LmZvY3VzKCk7XG4gIH1cblxuICAvKipcbiAgICogQmx1ciB0aGUgc2VhcmNoIGlucHV0XG4gICAqL1xuICBibHVyKCk6IHZvaWQge1xuICAgIHRoaXMuaW5wdXQ/LmJsdXIoKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDbGVhciB0aGUgc2VhcmNoIGlucHV0XG4gICAqL1xuICBjbGVhcigpOiB2b2lkIHtcbiAgICB0aGlzLnZhbHVlID0gJyc7XG4gIH1cblxuICBwcml2YXRlIHNldHVwTGlzdGVuZXJzKCk6IHZvaWQge1xuICAgIC8vIEtleWJvYXJkIHNob3J0Y3V0c1xuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLCAoZSkgPT4gdGhpcy5oYW5kbGVLZXlib2FyZChlKSk7XG5cbiAgICAvLyBJbnB1dCBoYW5kbGVyc1xuICAgIGlmICh0aGlzLmlucHV0KSB7XG4gICAgICB0aGlzLmlucHV0LmFkZEV2ZW50TGlzdGVuZXIoJ2lucHV0JywgKGUpID0+IHRoaXMuaGFuZGxlSW5wdXQoZSkpO1xuXG4gICAgICAvLyBQcmV2ZW50IGZvcm0gc3VibWlzc2lvbiBpZiB3cmFwcGVkIGluIGZvcm1cbiAgICAgIGNvbnN0IGZvcm0gPSB0aGlzLmlucHV0LmNsb3Nlc3QoJ2Zvcm0nKTtcbiAgICAgIGZvcm0/LmFkZEV2ZW50TGlzdGVuZXIoJ3N1Ym1pdCcsIChlKSA9PiB0aGlzLmhhbmRsZVN1Ym1pdChlKSk7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBoYW5kbGVLZXlib2FyZChlOiBLZXlib2FyZEV2ZW50KTogdm9pZCB7XG4gICAgLy8gQ21kL0N0cmwgKyBLIHRvIGZvY3VzIHNlYXJjaFxuICAgIGlmICgoZS5tZXRhS2V5IHx8IGUuY3RybEtleSkgJiYgZS5rZXkgPT09ICdrJykge1xuICAgICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xuICAgICAgdGhpcy5mb2N1cygpO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIC8vIEVzY2FwZSB0byBibHVyIHNlYXJjaCB3aGVuIGZvY3VzZWRcbiAgICBpZiAoZS5rZXkgPT09ICdFc2NhcGUnICYmIGRvY3VtZW50LmFjdGl2ZUVsZW1lbnQgPT09IHRoaXMuaW5wdXQpIHtcbiAgICAgIHRoaXMuYmx1cigpO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlSW5wdXQoZTogRXZlbnQpOiB2b2lkIHtcbiAgICBjb25zdCB0YXJnZXQgPSBlLnRhcmdldCBhcyBIVE1MSW5wdXRFbGVtZW50O1xuICAgIGNvbnN0IHF1ZXJ5ID0gdGFyZ2V0LnZhbHVlLnRyaW0oKTtcblxuICAgIC8vIEVtaXQgY3VzdG9tIGV2ZW50IGZvciBzZWFyY2hcbiAgICBkb2N1bWVudC5kaXNwYXRjaEV2ZW50KG5ldyBDdXN0b21FdmVudCgnYXBwOnNlYXJjaCcsIHtcbiAgICAgIGRldGFpbDogeyBxdWVyeSB9LFxuICAgICAgYnViYmxlczogdHJ1ZVxuICAgIH0pKTtcbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlU3VibWl0KGU6IEV2ZW50KTogdm9pZCB7XG4gICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xuXG4gICAgY29uc3QgcXVlcnkgPSB0aGlzLnZhbHVlLnRyaW0oKTtcbiAgICBpZiAoIXF1ZXJ5KSByZXR1cm47XG5cbiAgICAvLyBFbWl0IGN1c3RvbSBldmVudCBmb3Igc2VhcmNoIHN1Ym1pdFxuICAgIGRvY3VtZW50LmRpc3BhdGNoRXZlbnQobmV3IEN1c3RvbUV2ZW50KCdhcHA6c2VhcmNoLXN1Ym1pdCcsIHtcbiAgICAgIGRldGFpbDogeyBxdWVyeSB9LFxuICAgICAgYnViYmxlczogdHJ1ZVxuICAgIH0pKTtcbiAgfVxufVxuIiwgIi8qKlxuICogTG9jayBTY3JlZW4gQ29udHJvbGxlclxuICpcbiAqIEhhbmRsZXMgUElOLWJhc2VkIGxvY2sgc2NyZWVuIGZ1bmN0aW9uYWxpdHlcbiAqL1xuXG5pbXBvcnQgeyBEcmF3ZXJDb250cm9sbGVyIH0gZnJvbSAnLi9kcmF3ZXJzJztcblxuZXhwb3J0IGNsYXNzIExvY2tTY3JlZW5Db250cm9sbGVyIHtcbiAgcHJpdmF0ZSBzdGF0aWMgcmVhZG9ubHkgQ09SUkVDVF9QSU4gPSAnMTIzNCc7IC8vIERlbW8gUElOXG5cbiAgcHJpdmF0ZSBsb2NrU2NyZWVuOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIHBpbklucHV0OiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIHBpbktleXBhZDogSFRNTEVsZW1lbnQgfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBsb2NrVGltZUVsOiBIVE1MRWxlbWVudCB8IG51bGwgPSBudWxsO1xuICBwcml2YXRlIHBpbkRpZ2l0czogTm9kZUxpc3RPZjxIVE1MRWxlbWVudD4gfCBudWxsID0gbnVsbDtcbiAgcHJpdmF0ZSBjdXJyZW50UGluID0gJyc7XG4gIHByaXZhdGUgZHJhd2VyczogRHJhd2VyQ29udHJvbGxlciB8IG51bGwgPSBudWxsO1xuXG4gIGNvbnN0cnVjdG9yKGRyYXdlcnM/OiBEcmF3ZXJDb250cm9sbGVyKSB7XG4gICAgdGhpcy5kcmF3ZXJzID0gZHJhd2VycyA/PyBudWxsO1xuICAgIHRoaXMubG9ja1NjcmVlbiA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdsb2NrU2NyZWVuJyk7XG4gICAgdGhpcy5waW5JbnB1dCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdwaW5JbnB1dCcpO1xuICAgIHRoaXMucGluS2V5cGFkID0gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3BpbktleXBhZCcpO1xuICAgIHRoaXMubG9ja1RpbWVFbCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdsb2NrVGltZScpO1xuICAgIHRoaXMucGluRGlnaXRzID0gdGhpcy5waW5JbnB1dD8ucXVlcnlTZWxlY3RvckFsbDxIVE1MRWxlbWVudD4oJ3N3cC1waW4tZGlnaXQnKSA/PyBudWxsO1xuXG4gICAgdGhpcy5zZXR1cExpc3RlbmVycygpO1xuICB9XG5cbiAgLyoqXG4gICAqIENoZWNrIGlmIGxvY2sgc2NyZWVuIGlzIGFjdGl2ZVxuICAgKi9cbiAgZ2V0IGlzQWN0aXZlKCk6IGJvb2xlYW4ge1xuICAgIHJldHVybiB0aGlzLmxvY2tTY3JlZW4/LmNsYXNzTGlzdC5jb250YWlucygnYWN0aXZlJykgPz8gZmFsc2U7XG4gIH1cblxuICAvKipcbiAgICogU2hvdyB0aGUgbG9jayBzY3JlZW5cbiAgICovXG4gIHNob3coKTogdm9pZCB7XG4gICAgdGhpcy5kcmF3ZXJzPy5jbG9zZUFsbCgpO1xuXG4gICAgaWYgKHRoaXMubG9ja1NjcmVlbikge1xuICAgICAgdGhpcy5sb2NrU2NyZWVuLmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuICAgICAgZG9jdW1lbnQuYm9keS5zdHlsZS5vdmVyZmxvdyA9ICdoaWRkZW4nO1xuICAgIH1cblxuICAgIHRoaXMuY3VycmVudFBpbiA9ICcnO1xuICAgIHRoaXMudXBkYXRlRGlzcGxheSgpO1xuXG4gICAgLy8gVXBkYXRlIGxvY2sgdGltZVxuICAgIGlmICh0aGlzLmxvY2tUaW1lRWwpIHtcbiAgICAgIHRoaXMubG9ja1RpbWVFbC50ZXh0Q29udGVudCA9IGBMXHUwMEU1c3Qga2wuICR7dGhpcy5mb3JtYXRUaW1lKCl9YDtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogSGlkZSB0aGUgbG9jayBzY3JlZW5cbiAgICovXG4gIGhpZGUoKTogdm9pZCB7XG4gICAgaWYgKHRoaXMubG9ja1NjcmVlbikge1xuICAgICAgdGhpcy5sb2NrU2NyZWVuLmNsYXNzTGlzdC5yZW1vdmUoJ2FjdGl2ZScpO1xuICAgICAgZG9jdW1lbnQuYm9keS5zdHlsZS5vdmVyZmxvdyA9ICcnO1xuICAgIH1cblxuICAgIHRoaXMuY3VycmVudFBpbiA9ICcnO1xuICAgIHRoaXMudXBkYXRlRGlzcGxheSgpO1xuICB9XG5cbiAgcHJpdmF0ZSBmb3JtYXRUaW1lKCk6IHN0cmluZyB7XG4gICAgY29uc3Qgbm93ID0gbmV3IERhdGUoKTtcbiAgICBjb25zdCBob3VycyA9IG5vdy5nZXRIb3VycygpLnRvU3RyaW5nKCkucGFkU3RhcnQoMiwgJzAnKTtcbiAgICBjb25zdCBtaW51dGVzID0gbm93LmdldE1pbnV0ZXMoKS50b1N0cmluZygpLnBhZFN0YXJ0KDIsICcwJyk7XG4gICAgcmV0dXJuIGAke2hvdXJzfToke21pbnV0ZXN9YDtcbiAgfVxuXG4gIHByaXZhdGUgdXBkYXRlRGlzcGxheSgpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMucGluRGlnaXRzKSByZXR1cm47XG5cbiAgICB0aGlzLnBpbkRpZ2l0cy5mb3JFYWNoKChkaWdpdCwgaW5kZXgpID0+IHtcbiAgICAgIGRpZ2l0LmNsYXNzTGlzdC5yZW1vdmUoJ2ZpbGxlZCcsICdlcnJvcicpO1xuICAgICAgaWYgKGluZGV4IDwgdGhpcy5jdXJyZW50UGluLmxlbmd0aCkge1xuICAgICAgICBkaWdpdC50ZXh0Q29udGVudCA9ICdcdTIwMjInO1xuICAgICAgICBkaWdpdC5jbGFzc0xpc3QuYWRkKCdmaWxsZWQnKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGRpZ2l0LnRleHRDb250ZW50ID0gJyc7XG4gICAgICB9XG4gICAgfSk7XG4gIH1cblxuICBwcml2YXRlIHNob3dFcnJvcigpOiB2b2lkIHtcbiAgICBpZiAoIXRoaXMucGluRGlnaXRzKSByZXR1cm47XG5cbiAgICB0aGlzLnBpbkRpZ2l0cy5mb3JFYWNoKGRpZ2l0ID0+IGRpZ2l0LmNsYXNzTGlzdC5hZGQoJ2Vycm9yJykpO1xuXG4gICAgLy8gU2hha2UgYW5pbWF0aW9uXG4gICAgdGhpcy5waW5JbnB1dD8uY2xhc3NMaXN0LmFkZCgnc2hha2UnKTtcblxuICAgIHNldFRpbWVvdXQoKCkgPT4ge1xuICAgICAgdGhpcy5jdXJyZW50UGluID0gJyc7XG4gICAgICB0aGlzLnVwZGF0ZURpc3BsYXkoKTtcbiAgICAgIHRoaXMucGluSW5wdXQ/LmNsYXNzTGlzdC5yZW1vdmUoJ3NoYWtlJyk7XG4gICAgfSwgNTAwKTtcbiAgfVxuXG4gIHByaXZhdGUgdmVyaWZ5KCk6IHZvaWQge1xuICAgIGlmICh0aGlzLmN1cnJlbnRQaW4gPT09IExvY2tTY3JlZW5Db250cm9sbGVyLkNPUlJFQ1RfUElOKSB7XG4gICAgICB0aGlzLmhpZGUoKTtcbiAgICB9IGVsc2Uge1xuICAgICAgdGhpcy5zaG93RXJyb3IoKTtcbiAgICB9XG4gIH1cblxuICBwcml2YXRlIGFkZERpZ2l0KGRpZ2l0OiBzdHJpbmcpOiB2b2lkIHtcbiAgICBpZiAodGhpcy5jdXJyZW50UGluLmxlbmd0aCA+PSA0KSByZXR1cm47XG5cbiAgICB0aGlzLmN1cnJlbnRQaW4gKz0gZGlnaXQ7XG4gICAgdGhpcy51cGRhdGVEaXNwbGF5KCk7XG5cbiAgICAvLyBBdXRvLXZlcmlmeSB3aGVuIDQgZGlnaXRzIGVudGVyZWRcbiAgICBpZiAodGhpcy5jdXJyZW50UGluLmxlbmd0aCA9PT0gNCkge1xuICAgICAgc2V0VGltZW91dCgoKSA9PiB0aGlzLnZlcmlmeSgpLCAyMDApO1xuICAgIH1cbiAgfVxuXG4gIHByaXZhdGUgcmVtb3ZlRGlnaXQoKTogdm9pZCB7XG4gICAgaWYgKHRoaXMuY3VycmVudFBpbi5sZW5ndGggPT09IDApIHJldHVybjtcbiAgICB0aGlzLmN1cnJlbnRQaW4gPSB0aGlzLmN1cnJlbnRQaW4uc2xpY2UoMCwgLTEpO1xuICAgIHRoaXMudXBkYXRlRGlzcGxheSgpO1xuICB9XG5cbiAgcHJpdmF0ZSBjbGVhclBpbigpOiB2b2lkIHtcbiAgICB0aGlzLmN1cnJlbnRQaW4gPSAnJztcbiAgICB0aGlzLnVwZGF0ZURpc3BsYXkoKTtcbiAgfVxuXG4gIHByaXZhdGUgc2V0dXBMaXN0ZW5lcnMoKTogdm9pZCB7XG4gICAgLy8gS2V5cGFkIGNsaWNrIGhhbmRsZXJcbiAgICB0aGlzLnBpbktleXBhZD8uYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoZSkgPT4gdGhpcy5oYW5kbGVLZXlwYWRDbGljayhlKSk7XG5cbiAgICAvLyBLZXlib2FyZCBpbnB1dFxuICAgIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLCAoZSkgPT4gdGhpcy5oYW5kbGVLZXlib2FyZChlKSk7XG5cbiAgICAvLyBMb2NrIGJ1dHRvbiBpbiBzaWRlYmFyXG4gICAgZG9jdW1lbnQucXVlcnlTZWxlY3RvcjxIVE1MRWxlbWVudD4oJ3N3cC1zaWRlLW1lbnUtYWN0aW9uLmxvY2snKVxuICAgICAgPy5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHRoaXMuc2hvdygpKTtcbiAgfVxuXG4gIHByaXZhdGUgaGFuZGxlS2V5cGFkQ2xpY2soZTogRXZlbnQpOiB2b2lkIHtcbiAgICBjb25zdCB0YXJnZXQgPSBlLnRhcmdldCBhcyBIVE1MRWxlbWVudDtcbiAgICBjb25zdCBrZXkgPSB0YXJnZXQuY2xvc2VzdDxIVE1MRWxlbWVudD4oJ3N3cC1waW4ta2V5Jyk7XG5cbiAgICBpZiAoIWtleSkgcmV0dXJuO1xuXG4gICAgY29uc3QgZGlnaXQgPSBrZXkuZGF0YXNldC5kaWdpdDtcbiAgICBjb25zdCBhY3Rpb24gPSBrZXkuZGF0YXNldC5hY3Rpb247XG5cbiAgICBpZiAoZGlnaXQpIHtcbiAgICAgIHRoaXMuYWRkRGlnaXQoZGlnaXQpO1xuICAgIH0gZWxzZSBpZiAoYWN0aW9uID09PSAnYmFja3NwYWNlJykge1xuICAgICAgdGhpcy5yZW1vdmVEaWdpdCgpO1xuICAgIH0gZWxzZSBpZiAoYWN0aW9uID09PSAnY2xlYXInKSB7XG4gICAgICB0aGlzLmNsZWFyUGluKCk7XG4gICAgfVxuICB9XG5cbiAgcHJpdmF0ZSBoYW5kbGVLZXlib2FyZChlOiBLZXlib2FyZEV2ZW50KTogdm9pZCB7XG4gICAgaWYgKCF0aGlzLmlzQWN0aXZlKSByZXR1cm47XG5cbiAgICAvLyBQcmV2ZW50IGRlZmF1bHQgdG8gYXZvaWQgb3RoZXIgaW50ZXJhY3Rpb25zXG4gICAgZS5wcmV2ZW50RGVmYXVsdCgpO1xuXG4gICAgaWYgKGUua2V5ID49ICcwJyAmJiBlLmtleSA8PSAnOScpIHtcbiAgICAgIHRoaXMuYWRkRGlnaXQoZS5rZXkpO1xuICAgIH0gZWxzZSBpZiAoZS5rZXkgPT09ICdCYWNrc3BhY2UnKSB7XG4gICAgICB0aGlzLnJlbW92ZURpZ2l0KCk7XG4gICAgfSBlbHNlIGlmIChlLmtleSA9PT0gJ0VzY2FwZScpIHtcbiAgICAgIHRoaXMuY2xlYXJQaW4oKTtcbiAgICB9XG4gIH1cbn1cbiIsICIvKipcbiAqIFNhbG9uIE9TIEFwcFxuICpcbiAqIE1haW4gYXBwbGljYXRpb24gY2xhc3MgdGhhdCBvcmNoZXN0cmF0ZXMgYWxsIFVJIGNvbnRyb2xsZXJzXG4gKi9cblxuaW1wb3J0IHsgU2lkZWJhckNvbnRyb2xsZXIgfSBmcm9tICcuL21vZHVsZXMvc2lkZWJhcic7XG5pbXBvcnQgeyBEcmF3ZXJDb250cm9sbGVyIH0gZnJvbSAnLi9tb2R1bGVzL2RyYXdlcnMnO1xuaW1wb3J0IHsgVGhlbWVDb250cm9sbGVyIH0gZnJvbSAnLi9tb2R1bGVzL3RoZW1lJztcbmltcG9ydCB7IFNlYXJjaENvbnRyb2xsZXIgfSBmcm9tICcuL21vZHVsZXMvc2VhcmNoJztcbmltcG9ydCB7IExvY2tTY3JlZW5Db250cm9sbGVyIH0gZnJvbSAnLi9tb2R1bGVzL2xvY2tzY3JlZW4nO1xuXG4vKipcbiAqIE1haW4gYXBwbGljYXRpb24gY2xhc3NcbiAqL1xuZXhwb3J0IGNsYXNzIEFwcCB7XG4gIHJlYWRvbmx5IHNpZGViYXI6IFNpZGViYXJDb250cm9sbGVyO1xuICByZWFkb25seSBkcmF3ZXJzOiBEcmF3ZXJDb250cm9sbGVyO1xuICByZWFkb25seSB0aGVtZTogVGhlbWVDb250cm9sbGVyO1xuICByZWFkb25seSBzZWFyY2g6IFNlYXJjaENvbnRyb2xsZXI7XG4gIHJlYWRvbmx5IGxvY2tTY3JlZW46IExvY2tTY3JlZW5Db250cm9sbGVyO1xuXG4gIGNvbnN0cnVjdG9yKCkge1xuICAgIC8vIEluaXRpYWxpemUgY29udHJvbGxlcnNcbiAgICB0aGlzLnNpZGViYXIgPSBuZXcgU2lkZWJhckNvbnRyb2xsZXIoKTtcbiAgICB0aGlzLmRyYXdlcnMgPSBuZXcgRHJhd2VyQ29udHJvbGxlcigpO1xuICAgIHRoaXMudGhlbWUgPSBuZXcgVGhlbWVDb250cm9sbGVyKCk7XG4gICAgdGhpcy5zZWFyY2ggPSBuZXcgU2VhcmNoQ29udHJvbGxlcigpO1xuICAgIHRoaXMubG9ja1NjcmVlbiA9IG5ldyBMb2NrU2NyZWVuQ29udHJvbGxlcih0aGlzLmRyYXdlcnMpO1xuICB9XG59XG5cbi8qKlxuICogR2xvYmFsIGFwcCBpbnN0YW5jZVxuICovXG5sZXQgYXBwOiBBcHA7XG5cbi8qKlxuICogSW5pdGlhbGl6ZSB0aGUgYXBwbGljYXRpb25cbiAqL1xuZnVuY3Rpb24gaW5pdCgpOiB2b2lkIHtcbiAgYXBwID0gbmV3IEFwcCgpO1xuXG4gIC8vIEV4cG9zZSB0byB3aW5kb3cgZm9yIGRlYnVnZ2luZ1xuICBpZiAodHlwZW9mIHdpbmRvdyAhPT0gJ3VuZGVmaW5lZCcpIHtcbiAgICAod2luZG93IGFzIHVua25vd24gYXMgeyBhcHA6IEFwcCB9KS5hcHAgPSBhcHA7XG4gIH1cbn1cblxuLy8gV2FpdCBmb3IgRE9NIHJlYWR5XG5pZiAoZG9jdW1lbnQucmVhZHlTdGF0ZSA9PT0gJ2xvYWRpbmcnKSB7XG4gIGRvY3VtZW50LmFkZEV2ZW50TGlzdGVuZXIoJ0RPTUNvbnRlbnRMb2FkZWQnLCBpbml0KTtcbn0gZWxzZSB7XG4gIGluaXQoKTtcbn1cblxuZXhwb3J0IHsgYXBw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 diff --git a/PlanTempus.Application/wwwroot/ts/app.ts b/PlanTempus.Application/wwwroot/ts/app.ts new file mode 100644 index 0000000..16735f5 --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/app.ts @@ -0,0 +1,58 @@ +/** + * Salon OS App + * + * Main application class that orchestrates all UI controllers + */ + +import { SidebarController } from './modules/sidebar'; +import { DrawerController } from './modules/drawers'; +import { ThemeController } from './modules/theme'; +import { SearchController } from './modules/search'; +import { LockScreenController } from './modules/lockscreen'; + +/** + * Main application class + */ +export class App { + readonly sidebar: SidebarController; + readonly drawers: DrawerController; + readonly theme: ThemeController; + readonly search: SearchController; + readonly lockScreen: LockScreenController; + + constructor() { + // Initialize controllers + this.sidebar = new SidebarController(); + this.drawers = new DrawerController(); + this.theme = new ThemeController(); + this.search = new SearchController(); + this.lockScreen = new LockScreenController(this.drawers); + } +} + +/** + * Global app instance + */ +let app: App; + +/** + * Initialize the application + */ +function init(): void { + app = new App(); + + // Expose to window for debugging + if (typeof window !== 'undefined') { + (window as unknown as { app: App }).app = app; + } +} + +// Wait for DOM ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} + +export { app }; +export default App; diff --git a/PlanTempus.Application/wwwroot/ts/modules/drawers.ts b/PlanTempus.Application/wwwroot/ts/modules/drawers.ts new file mode 100644 index 0000000..7ee36ad --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/modules/drawers.ts @@ -0,0 +1,226 @@ +/** + * Drawer Controller + * + * Handles all drawer functionality including profile, notifications, and todo drawers + */ + +export type DrawerName = 'profile' | 'notification' | 'todo' | 'newTodo'; + +export class DrawerController { + private profileDrawer: HTMLElement | null = null; + private notificationDrawer: HTMLElement | null = null; + private todoDrawer: HTMLElement | null = null; + private newTodoDrawer: HTMLElement | null = null; + private overlay: HTMLElement | null = null; + private activeDrawer: DrawerName | null = null; + + constructor() { + this.profileDrawer = document.getElementById('profileDrawer'); + this.notificationDrawer = document.getElementById('notificationDrawer'); + this.todoDrawer = document.getElementById('todoDrawer'); + this.newTodoDrawer = document.getElementById('newTodoDrawer'); + this.overlay = document.getElementById('drawerOverlay'); + + this.setupListeners(); + } + + /** + * Get currently active drawer name + */ + get active(): DrawerName | null { + return this.activeDrawer; + } + + /** + * Open a drawer by name + */ + open(name: DrawerName): void { + this.closeAll(); + + const drawer = this.getDrawer(name); + if (drawer && this.overlay) { + drawer.classList.add('active'); + this.overlay.classList.add('active'); + document.body.style.overflow = 'hidden'; + this.activeDrawer = name; + } + } + + /** + * Close a specific drawer + */ + close(name: DrawerName): void { + const drawer = this.getDrawer(name); + drawer?.classList.remove('active'); + + // Only hide overlay if no drawers are active + if (this.overlay && !document.querySelector('.active[class*="drawer"]')) { + this.overlay.classList.remove('active'); + document.body.style.overflow = ''; + } + + if (this.activeDrawer === name) { + this.activeDrawer = null; + } + } + + /** + * Close all drawers + */ + closeAll(): void { + [this.profileDrawer, this.notificationDrawer, this.todoDrawer, this.newTodoDrawer] + .forEach(drawer => drawer?.classList.remove('active')); + + this.overlay?.classList.remove('active'); + document.body.style.overflow = ''; + this.activeDrawer = null; + } + + /** + * Open profile drawer + */ + openProfile(): void { + this.open('profile'); + } + + /** + * Open notification drawer + */ + openNotification(): void { + this.open('notification'); + } + + /** + * Open todo drawer (slides on top of profile) + */ + openTodo(): void { + this.todoDrawer?.classList.add('active'); + } + + /** + * Close todo drawer + */ + closeTodo(): void { + this.todoDrawer?.classList.remove('active'); + this.closeNewTodo(); + } + + /** + * Open new todo drawer + */ + openNewTodo(): void { + this.newTodoDrawer?.classList.add('active'); + } + + /** + * Close new todo drawer + */ + closeNewTodo(): void { + this.newTodoDrawer?.classList.remove('active'); + } + + /** + * Mark all notifications as read + */ + markAllNotificationsRead(): void { + if (!this.notificationDrawer) return; + + const unreadItems = this.notificationDrawer.querySelectorAll( + 'swp-notification-item[data-unread="true"]' + ); + unreadItems.forEach(item => item.removeAttribute('data-unread')); + + const badge = document.querySelector('swp-notification-badge'); + if (badge) { + badge.style.display = 'none'; + } + } + + private getDrawer(name: DrawerName): HTMLElement | null { + switch (name) { + case 'profile': return this.profileDrawer; + case 'notification': return this.notificationDrawer; + case 'todo': return this.todoDrawer; + case 'newTodo': return this.newTodoDrawer; + } + } + + private setupListeners(): void { + // Profile drawer triggers + document.getElementById('profileTrigger') + ?.addEventListener('click', () => this.openProfile()); + document.getElementById('drawerClose') + ?.addEventListener('click', () => this.close('profile')); + + // Notification drawer triggers + document.getElementById('notificationsBtn') + ?.addEventListener('click', () => this.openNotification()); + document.getElementById('notificationDrawerClose') + ?.addEventListener('click', () => this.close('notification')); + document.getElementById('markAllRead') + ?.addEventListener('click', () => this.markAllNotificationsRead()); + + // Todo drawer triggers + document.getElementById('openTodoDrawer') + ?.addEventListener('click', () => this.openTodo()); + document.getElementById('todoDrawerBack') + ?.addEventListener('click', () => this.closeTodo()); + + // New todo drawer triggers + document.getElementById('addTodoBtn') + ?.addEventListener('click', () => this.openNewTodo()); + document.getElementById('newTodoDrawerBack') + ?.addEventListener('click', () => this.closeNewTodo()); + document.getElementById('cancelNewTodo') + ?.addEventListener('click', () => this.closeNewTodo()); + document.getElementById('saveNewTodo') + ?.addEventListener('click', () => this.closeNewTodo()); + + // Overlay click closes all + this.overlay?.addEventListener('click', () => this.closeAll()); + + // Escape key closes all + document.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Escape') this.closeAll(); + }); + + // Todo interactions + this.todoDrawer?.addEventListener('click', (e) => this.handleTodoClick(e)); + + // Visibility options + document.addEventListener('click', (e) => this.handleVisibilityClick(e)); + } + + private handleTodoClick(e: Event): void { + const target = e.target as HTMLElement; + const todoItem = target.closest('swp-todo-item'); + const checkbox = target.closest('swp-todo-checkbox'); + + if (checkbox && todoItem) { + const isCompleted = todoItem.dataset.completed === 'true'; + if (isCompleted) { + todoItem.removeAttribute('data-completed'); + } else { + todoItem.dataset.completed = 'true'; + } + } + + // Toggle section collapse + const sectionHeader = target.closest('swp-todo-section-header'); + if (sectionHeader) { + const section = sectionHeader.closest('swp-todo-section'); + section?.classList.toggle('collapsed'); + } + } + + private handleVisibilityClick(e: Event): void { + const target = e.target as HTMLElement; + const option = target.closest('swp-visibility-option'); + + if (option) { + document.querySelectorAll('swp-visibility-option') + .forEach(o => o.classList.remove('active')); + option.classList.add('active'); + } + } +} diff --git a/PlanTempus.Application/wwwroot/ts/modules/lockscreen.ts b/PlanTempus.Application/wwwroot/ts/modules/lockscreen.ts new file mode 100644 index 0000000..f6a5878 --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/modules/lockscreen.ts @@ -0,0 +1,182 @@ +/** + * Lock Screen Controller + * + * Handles PIN-based lock screen functionality + */ + +import { DrawerController } from './drawers'; + +export class LockScreenController { + private static readonly CORRECT_PIN = '1234'; // Demo PIN + + private lockScreen: HTMLElement | null = null; + private pinInput: HTMLElement | null = null; + private pinKeypad: HTMLElement | null = null; + private lockTimeEl: HTMLElement | null = null; + private pinDigits: NodeListOf | null = null; + private currentPin = ''; + private drawers: DrawerController | null = null; + + constructor(drawers?: DrawerController) { + this.drawers = drawers ?? null; + this.lockScreen = document.getElementById('lockScreen'); + this.pinInput = document.getElementById('pinInput'); + this.pinKeypad = document.getElementById('pinKeypad'); + this.lockTimeEl = document.getElementById('lockTime'); + this.pinDigits = this.pinInput?.querySelectorAll('swp-pin-digit') ?? null; + + this.setupListeners(); + } + + /** + * Check if lock screen is active + */ + get isActive(): boolean { + return this.lockScreen?.classList.contains('active') ?? false; + } + + /** + * Show the lock screen + */ + show(): void { + this.drawers?.closeAll(); + + if (this.lockScreen) { + this.lockScreen.classList.add('active'); + document.body.style.overflow = 'hidden'; + } + + this.currentPin = ''; + this.updateDisplay(); + + // Update lock time + if (this.lockTimeEl) { + this.lockTimeEl.textContent = `Låst kl. ${this.formatTime()}`; + } + } + + /** + * Hide the lock screen + */ + hide(): void { + if (this.lockScreen) { + this.lockScreen.classList.remove('active'); + document.body.style.overflow = ''; + } + + this.currentPin = ''; + this.updateDisplay(); + } + + private formatTime(): string { + const now = new Date(); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; + } + + private updateDisplay(): void { + if (!this.pinDigits) return; + + this.pinDigits.forEach((digit, index) => { + digit.classList.remove('filled', 'error'); + if (index < this.currentPin.length) { + digit.textContent = '•'; + digit.classList.add('filled'); + } else { + digit.textContent = ''; + } + }); + } + + private showError(): void { + if (!this.pinDigits) return; + + this.pinDigits.forEach(digit => digit.classList.add('error')); + + // Shake animation + this.pinInput?.classList.add('shake'); + + setTimeout(() => { + this.currentPin = ''; + this.updateDisplay(); + this.pinInput?.classList.remove('shake'); + }, 500); + } + + private verify(): void { + if (this.currentPin === LockScreenController.CORRECT_PIN) { + this.hide(); + } else { + this.showError(); + } + } + + private addDigit(digit: string): void { + if (this.currentPin.length >= 4) return; + + this.currentPin += digit; + this.updateDisplay(); + + // Auto-verify when 4 digits entered + if (this.currentPin.length === 4) { + setTimeout(() => this.verify(), 200); + } + } + + private removeDigit(): void { + if (this.currentPin.length === 0) return; + this.currentPin = this.currentPin.slice(0, -1); + this.updateDisplay(); + } + + private clearPin(): void { + this.currentPin = ''; + this.updateDisplay(); + } + + private setupListeners(): void { + // Keypad click handler + this.pinKeypad?.addEventListener('click', (e) => this.handleKeypadClick(e)); + + // Keyboard input + document.addEventListener('keydown', (e) => this.handleKeyboard(e)); + + // Lock button in sidebar + document.querySelector('swp-side-menu-action.lock') + ?.addEventListener('click', () => this.show()); + } + + private handleKeypadClick(e: Event): void { + const target = e.target as HTMLElement; + const key = target.closest('swp-pin-key'); + + if (!key) return; + + const digit = key.dataset.digit; + const action = key.dataset.action; + + if (digit) { + this.addDigit(digit); + } else if (action === 'backspace') { + this.removeDigit(); + } else if (action === 'clear') { + this.clearPin(); + } + } + + private handleKeyboard(e: KeyboardEvent): void { + if (!this.isActive) return; + + // Prevent default to avoid other interactions + e.preventDefault(); + + if (e.key >= '0' && e.key <= '9') { + this.addDigit(e.key); + } else if (e.key === 'Backspace') { + this.removeDigit(); + } else if (e.key === 'Escape') { + this.clearPin(); + } + } +} diff --git a/PlanTempus.Application/wwwroot/ts/modules/search.ts b/PlanTempus.Application/wwwroot/ts/modules/search.ts new file mode 100644 index 0000000..d6eac21 --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/modules/search.ts @@ -0,0 +1,106 @@ +/** + * Search Controller + * + * Handles global search functionality and keyboard shortcuts + */ + +export class SearchController { + private input: HTMLInputElement | null = null; + private container: HTMLElement | null = null; + + constructor() { + this.input = document.getElementById('globalSearch') as HTMLInputElement | null; + this.container = document.querySelector('swp-topbar-search'); + + this.setupListeners(); + } + + /** + * Get current search value + */ + get value(): string { + return this.input?.value ?? ''; + } + + /** + * Set search value + */ + set value(val: string) { + if (this.input) { + this.input.value = val; + } + } + + /** + * Focus the search input + */ + focus(): void { + this.input?.focus(); + } + + /** + * Blur the search input + */ + blur(): void { + this.input?.blur(); + } + + /** + * Clear the search input + */ + clear(): void { + this.value = ''; + } + + private setupListeners(): void { + // Keyboard shortcuts + document.addEventListener('keydown', (e) => this.handleKeyboard(e)); + + // Input handlers + if (this.input) { + this.input.addEventListener('input', (e) => this.handleInput(e)); + + // Prevent form submission if wrapped in form + const form = this.input.closest('form'); + form?.addEventListener('submit', (e) => this.handleSubmit(e)); + } + } + + private handleKeyboard(e: KeyboardEvent): void { + // Cmd/Ctrl + K to focus search + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + this.focus(); + return; + } + + // Escape to blur search when focused + if (e.key === 'Escape' && document.activeElement === this.input) { + this.blur(); + } + } + + private handleInput(e: Event): void { + const target = e.target as HTMLInputElement; + const query = target.value.trim(); + + // Emit custom event for search + document.dispatchEvent(new CustomEvent('app:search', { + detail: { query }, + bubbles: true + })); + } + + private handleSubmit(e: Event): void { + e.preventDefault(); + + const query = this.value.trim(); + if (!query) return; + + // Emit custom event for search submit + document.dispatchEvent(new CustomEvent('app:search-submit', { + detail: { query }, + bubbles: true + })); + } +} diff --git a/PlanTempus.Application/wwwroot/ts/modules/sidebar.ts b/PlanTempus.Application/wwwroot/ts/modules/sidebar.ts new file mode 100644 index 0000000..ddb0a2b --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/modules/sidebar.ts @@ -0,0 +1,96 @@ +/** + * Sidebar Controller + * + * Handles sidebar collapse/expand and tooltip functionality + */ + +export class SidebarController { + private menuToggle: HTMLElement | null = null; + private appLayout: HTMLElement | null = null; + private menuTooltip: HTMLElement | null = null; + + constructor() { + this.menuToggle = document.getElementById('menuToggle'); + this.appLayout = document.querySelector('swp-app-layout'); + this.menuTooltip = document.getElementById('menuTooltip'); + + this.setupListeners(); + this.setupTooltips(); + this.restoreState(); + } + + /** + * Check if sidebar is collapsed + */ + get isCollapsed(): boolean { + return this.appLayout?.classList.contains('menu-collapsed') ?? false; + } + + /** + * Toggle sidebar collapsed state + */ + toggle(): void { + if (!this.appLayout) return; + + this.appLayout.classList.toggle('menu-collapsed'); + localStorage.setItem('sidebar-collapsed', String(this.isCollapsed)); + } + + /** + * Collapse the sidebar + */ + collapse(): void { + this.appLayout?.classList.add('menu-collapsed'); + localStorage.setItem('sidebar-collapsed', 'true'); + } + + /** + * Expand the sidebar + */ + expand(): void { + this.appLayout?.classList.remove('menu-collapsed'); + localStorage.setItem('sidebar-collapsed', 'false'); + } + + private setupListeners(): void { + this.menuToggle?.addEventListener('click', () => this.toggle()); + } + + private setupTooltips(): void { + if (!this.menuTooltip) return; + + const menuItems = document.querySelectorAll('swp-side-menu-item[data-tooltip]'); + + menuItems.forEach(item => { + item.addEventListener('mouseenter', () => this.showTooltip(item)); + item.addEventListener('mouseleave', () => this.hideTooltip()); + }); + } + + private showTooltip(item: HTMLElement): void { + if (!this.isCollapsed || !this.menuTooltip) return; + + const rect = item.getBoundingClientRect(); + const tooltipText = item.dataset.tooltip; + + if (!tooltipText) return; + + this.menuTooltip.textContent = tooltipText; + this.menuTooltip.style.left = `${rect.right + 8}px`; + this.menuTooltip.style.top = `${rect.top + rect.height / 2}px`; + this.menuTooltip.style.transform = 'translateY(-50%)'; + this.menuTooltip.showPopover(); + } + + private hideTooltip(): void { + this.menuTooltip?.hidePopover(); + } + + private restoreState(): void { + if (!this.appLayout) return; + + if (localStorage.getItem('sidebar-collapsed') === 'true') { + this.appLayout.classList.add('menu-collapsed'); + } + } +} diff --git a/PlanTempus.Application/wwwroot/ts/modules/theme.ts b/PlanTempus.Application/wwwroot/ts/modules/theme.ts new file mode 100644 index 0000000..b14e4b5 --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/modules/theme.ts @@ -0,0 +1,120 @@ +/** + * Theme Controller + * + * Handles dark/light mode switching and system preference detection + */ + +export type Theme = 'light' | 'dark' | 'system'; + +export class ThemeController { + private static readonly STORAGE_KEY = 'theme-preference'; + private static readonly DARK_CLASS = 'dark-mode'; + private static readonly LIGHT_CLASS = 'light-mode'; + + private root: HTMLElement; + private themeOptions: NodeListOf; + + constructor() { + this.root = document.documentElement; + this.themeOptions = document.querySelectorAll('swp-theme-option'); + + this.applyTheme(this.current); + this.updateUI(); + this.setupListeners(); + } + + /** + * Get the current theme setting + */ + get current(): Theme { + const stored = localStorage.getItem(ThemeController.STORAGE_KEY) as Theme | null; + if (stored === 'dark' || stored === 'light' || stored === 'system') { + return stored; + } + return 'system'; + } + + /** + * Check if dark mode is currently active + */ + get isDark(): boolean { + return this.root.classList.contains(ThemeController.DARK_CLASS) || + (this.systemPrefersDark && !this.root.classList.contains(ThemeController.LIGHT_CLASS)); + } + + /** + * Check if system prefers dark mode + */ + get systemPrefersDark(): boolean { + return window.matchMedia('(prefers-color-scheme: dark)').matches; + } + + /** + * Set theme and persist preference + */ + set(theme: Theme): void { + localStorage.setItem(ThemeController.STORAGE_KEY, theme); + this.applyTheme(theme); + this.updateUI(); + } + + /** + * Toggle between light and dark themes + */ + toggle(): void { + this.set(this.isDark ? 'light' : 'dark'); + } + + private applyTheme(theme: Theme): void { + this.root.classList.remove(ThemeController.DARK_CLASS, ThemeController.LIGHT_CLASS); + + if (theme === 'dark') { + this.root.classList.add(ThemeController.DARK_CLASS); + } else if (theme === 'light') { + this.root.classList.add(ThemeController.LIGHT_CLASS); + } + // 'system' leaves both classes off, letting CSS media query handle it + } + + private updateUI(): void { + if (!this.themeOptions) return; + + const darkActive = this.isDark; + + this.themeOptions.forEach(option => { + const theme = option.dataset.theme as Theme; + const isActive = (theme === 'dark' && darkActive) || (theme === 'light' && !darkActive); + option.classList.toggle('active', isActive); + }); + } + + private setupListeners(): void { + // Theme option clicks + this.themeOptions.forEach(option => { + option.addEventListener('click', (e) => this.handleOptionClick(e)); + }); + + // System theme changes + window.matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', () => this.handleSystemChange()); + } + + private handleOptionClick(e: Event): void { + const target = e.target as HTMLElement; + const option = target.closest('swp-theme-option'); + + if (option) { + const theme = option.dataset.theme as Theme; + if (theme) { + this.set(theme); + } + } + } + + private handleSystemChange(): void { + // Only react to system changes if we're using system preference + if (this.current === 'system') { + this.updateUI(); + } + } +} diff --git a/PlanTempus.Application/wwwroot/ts/tsconfig.json b/PlanTempus.Application/wwwroot/ts/tsconfig.json new file mode 100644 index 0000000..87d8c61 --- /dev/null +++ b/PlanTempus.Application/wwwroot/ts/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": false, + "outDir": "../js/app", + "rootDir": ".", + "sourceMap": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"] + }, + "include": [ + "./**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/PlanTempus.Components/Outbox/OutboxListener.cs b/PlanTempus.Components/Outbox/OutboxListener.cs index 80578dd..19d43cc 100644 --- a/PlanTempus.Components/Outbox/OutboxListener.cs +++ b/PlanTempus.Components/Outbox/OutboxListener.cs @@ -2,6 +2,7 @@ using Microsoft.ApplicationInsights; using Microsoft.Extensions.Hosting; using Npgsql; using PlanTempus.Core.Database.ConnectionFactory; +using PlanTempus.Core.Telemetry; namespace PlanTempus.Components.Outbox; @@ -10,19 +11,23 @@ public class OutboxListener : BackgroundService private readonly IDbConnectionFactory _connectionFactory; private readonly ICommandHandler _commandHandler; private readonly TelemetryClient _telemetryClient; + private readonly IMessageChannel _notificationChannel; public OutboxListener( IDbConnectionFactory connectionFactory, ICommandHandler commandHandler, - TelemetryClient telemetryClient) + TelemetryClient telemetryClient, + IMessageChannel notificationChannel) { _connectionFactory = connectionFactory; _commandHandler = commandHandler; _telemetryClient = telemetryClient; + _notificationChannel = notificationChannel; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + Console.WriteLine("OutboxListener starting - listening for outbox_messages"); _telemetryClient.TrackTrace("OutboxListener starting - listening for outbox_messages"); while (!stoppingToken.IsCancellationRequested) @@ -33,6 +38,7 @@ public class OutboxListener : BackgroundService } catch (Exception ex) when (ex is not OperationCanceledException) { + Console.WriteLine($"OutboxListener error: {ex.Message}"); _telemetryClient.TrackException(ex); await Task.Delay(5000, stoppingToken); } @@ -44,18 +50,10 @@ public class OutboxListener : BackgroundService await using var conn = (NpgsqlConnection)_connectionFactory.Create(); await conn.OpenAsync(stoppingToken); - conn.Notification += async (_, e) => + conn.Notification += (_, e) => { - _telemetryClient.TrackTrace($"Outbox notification received: {e.Payload}"); - - try - { - await _commandHandler.Handle(new ProcessOutboxCommand()); - } - catch (Exception ex) - { - _telemetryClient.TrackException(ex); - } + Console.WriteLine($"Notification event received: {e.Payload}"); + _notificationChannel.Writer.TryWrite(e.Payload); }; await using (var cmd = new NpgsqlCommand("LISTEN outbox_messages;", conn)) @@ -63,14 +61,32 @@ public class OutboxListener : BackgroundService await cmd.ExecuteNonQueryAsync(stoppingToken); } + Console.WriteLine("OutboxListener now listening on outbox_messages channel"); _telemetryClient.TrackTrace("OutboxListener now listening on outbox_messages channel"); // Process any pending messages on startup + Console.WriteLine("Processing pending messages on startup..."); await _commandHandler.Handle(new ProcessOutboxCommand()); while (!stoppingToken.IsCancellationRequested) { await conn.WaitAsync(stoppingToken); + + while (_notificationChannel.Reader.TryRead(out var payload)) + { + Console.WriteLine($"Outbox notification received from channel: {payload}"); + _telemetryClient.TrackTrace($"Outbox notification received: {payload}"); + + try + { + await _commandHandler.Handle(new ProcessOutboxCommand()); + } + catch (Exception ex) + { + Console.WriteLine($"Error processing outbox: {ex.Message}"); + _telemetryClient.TrackException(ex); + } + } } } diff --git a/PlanTempus.Components/Outbox/ProcessOutboxHandler.cs b/PlanTempus.Components/Outbox/ProcessOutboxHandler.cs index cb62edd..6315869 100644 --- a/PlanTempus.Components/Outbox/ProcessOutboxHandler.cs +++ b/PlanTempus.Components/Outbox/ProcessOutboxHandler.cs @@ -13,23 +13,28 @@ public class ProcessOutboxHandler( { public async Task Handle(ProcessOutboxCommand command) { + Console.WriteLine($"ProcessOutboxHandler started"); telemetryClient.TrackTrace($"ProcessOutboxHandler started"); var messages = await outboxService.GetPendingAsync(command.BatchSize); + Console.WriteLine($"ProcessOutboxHandler found {messages.Count} pending messages"); telemetryClient.TrackTrace($"ProcessOutboxHandler found {messages.Count} pending messages"); foreach (var message in messages) { try { + Console.WriteLine($"Processing message {message.Id} of type {message.Type}"); telemetryClient.TrackTrace($"Processing message {message.Id} of type {message.Type}"); await ProcessMessageAsync(message); await outboxService.MarkAsSentAsync(message.Id); + Console.WriteLine($"Message {message.Id} marked as sent"); telemetryClient.TrackTrace($"Message {message.Id} marked as sent"); } catch (Exception ex) { + Console.WriteLine($"Message {message.Id} failed: {ex.Message}"); telemetryClient.TrackTrace($"Message {message.Id} failed: {ex.Message}"); await outboxService.MarkAsFailedAsync(message.Id, ex.Message); } diff --git a/PlanTempus.Components/PlanTempus.Components.csproj b/PlanTempus.Components/PlanTempus.Components.csproj index 2ea9caa..4dd8281 100644 --- a/PlanTempus.Components/PlanTempus.Components.csproj +++ b/PlanTempus.Components/PlanTempus.Components.csproj @@ -5,10 +5,6 @@ enable - - - - @@ -21,4 +17,8 @@ + + + + diff --git a/Core/CommandQueries/Command.cs b/PlanTempus.Core/CommandQueries/Command.cs similarity index 100% rename from Core/CommandQueries/Command.cs rename to PlanTempus.Core/CommandQueries/Command.cs diff --git a/Core/CommandQueries/CommandResponse.cs b/PlanTempus.Core/CommandQueries/CommandResponse.cs similarity index 100% rename from Core/CommandQueries/CommandResponse.cs rename to PlanTempus.Core/CommandQueries/CommandResponse.cs diff --git a/Core/CommandQueries/ICommand.cs b/PlanTempus.Core/CommandQueries/ICommand.cs similarity index 100% rename from Core/CommandQueries/ICommand.cs rename to PlanTempus.Core/CommandQueries/ICommand.cs diff --git a/Core/CommandQueries/ProblemDetails.cs b/PlanTempus.Core/CommandQueries/ProblemDetails.cs similarity index 100% rename from Core/CommandQueries/ProblemDetails.cs rename to PlanTempus.Core/CommandQueries/ProblemDetails.cs diff --git a/Core/Configurations/Common/KeyValueToJson.cs b/PlanTempus.Core/Configurations/Common/KeyValueToJson.cs similarity index 100% rename from Core/Configurations/Common/KeyValueToJson.cs rename to PlanTempus.Core/Configurations/Common/KeyValueToJson.cs diff --git a/Core/Configurations/ConfigurationBuilder.cs b/PlanTempus.Core/Configurations/ConfigurationBuilder.cs similarity index 100% rename from Core/Configurations/ConfigurationBuilder.cs rename to PlanTempus.Core/Configurations/ConfigurationBuilder.cs diff --git a/Core/Configurations/IAppConfiguration.cs b/PlanTempus.Core/Configurations/IAppConfiguration.cs similarity index 100% rename from Core/Configurations/IAppConfiguration.cs rename to PlanTempus.Core/Configurations/IAppConfiguration.cs diff --git a/Core/Configurations/IConfigurationRoot.cs b/PlanTempus.Core/Configurations/IConfigurationRoot.cs similarity index 100% rename from Core/Configurations/IConfigurationRoot.cs rename to PlanTempus.Core/Configurations/IConfigurationRoot.cs diff --git a/Core/Configurations/JsonConfigProvider/JsonConfigExtension.cs b/PlanTempus.Core/Configurations/JsonConfigProvider/JsonConfigExtension.cs similarity index 100% rename from Core/Configurations/JsonConfigProvider/JsonConfigExtension.cs rename to PlanTempus.Core/Configurations/JsonConfigProvider/JsonConfigExtension.cs diff --git a/Core/Configurations/SmartConfigProvider/AppConfiguration.cs b/PlanTempus.Core/Configurations/SmartConfigProvider/AppConfiguration.cs similarity index 100% rename from Core/Configurations/SmartConfigProvider/AppConfiguration.cs rename to PlanTempus.Core/Configurations/SmartConfigProvider/AppConfiguration.cs diff --git a/Core/Configurations/SmartConfigProvider/IConfigurationRepository.cs b/PlanTempus.Core/Configurations/SmartConfigProvider/IConfigurationRepository.cs similarity index 100% rename from Core/Configurations/SmartConfigProvider/IConfigurationRepository.cs rename to PlanTempus.Core/Configurations/SmartConfigProvider/IConfigurationRepository.cs diff --git a/Core/Configurations/SmartConfigProvider/Repositories/PostgresConfigurationRepository.cs b/PlanTempus.Core/Configurations/SmartConfigProvider/Repositories/PostgresConfigurationRepository.cs similarity index 100% rename from Core/Configurations/SmartConfigProvider/Repositories/PostgresConfigurationRepository.cs rename to PlanTempus.Core/Configurations/SmartConfigProvider/Repositories/PostgresConfigurationRepository.cs diff --git a/Core/Configurations/SmartConfigProvider/SmartConfigExtension.cs b/PlanTempus.Core/Configurations/SmartConfigProvider/SmartConfigExtension.cs similarity index 100% rename from Core/Configurations/SmartConfigProvider/SmartConfigExtension.cs rename to PlanTempus.Core/Configurations/SmartConfigProvider/SmartConfigExtension.cs diff --git a/Core/Configurations/SmartConfigProvider/SmartConfigOptions.cs b/PlanTempus.Core/Configurations/SmartConfigProvider/SmartConfigOptions.cs similarity index 100% rename from Core/Configurations/SmartConfigProvider/SmartConfigOptions.cs rename to PlanTempus.Core/Configurations/SmartConfigProvider/SmartConfigOptions.cs diff --git a/Core/Configurations/SmartConfigProvider/SmartConfigProvider.cs b/PlanTempus.Core/Configurations/SmartConfigProvider/SmartConfigProvider.cs similarity index 100% rename from Core/Configurations/SmartConfigProvider/SmartConfigProvider.cs rename to PlanTempus.Core/Configurations/SmartConfigProvider/SmartConfigProvider.cs diff --git a/Core/Database/ConnectionFactory/IDbConnectionFactory.cs b/PlanTempus.Core/Database/ConnectionFactory/IDbConnectionFactory.cs similarity index 100% rename from Core/Database/ConnectionFactory/IDbConnectionFactory.cs rename to PlanTempus.Core/Database/ConnectionFactory/IDbConnectionFactory.cs diff --git a/Core/Database/ConnectionFactory/PostgresConnectionFactory.cs b/PlanTempus.Core/Database/ConnectionFactory/PostgresConnectionFactory.cs similarity index 100% rename from Core/Database/ConnectionFactory/PostgresConnectionFactory.cs rename to PlanTempus.Core/Database/ConnectionFactory/PostgresConnectionFactory.cs diff --git a/Core/Database/DatabaseScope.cs b/PlanTempus.Core/Database/DatabaseScope.cs similarity index 100% rename from Core/Database/DatabaseScope.cs rename to PlanTempus.Core/Database/DatabaseScope.cs diff --git a/Core/Database/IDatabaseOperations.cs b/PlanTempus.Core/Database/IDatabaseOperations.cs similarity index 100% rename from Core/Database/IDatabaseOperations.cs rename to PlanTempus.Core/Database/IDatabaseOperations.cs diff --git a/Core/Database/SqlOperations.cs b/PlanTempus.Core/Database/SqlOperations.cs similarity index 100% rename from Core/Database/SqlOperations.cs rename to PlanTempus.Core/Database/SqlOperations.cs diff --git a/Core/Email/EmailModule.cs b/PlanTempus.Core/Email/EmailModule.cs similarity index 100% rename from Core/Email/EmailModule.cs rename to PlanTempus.Core/Email/EmailModule.cs diff --git a/Core/Email/IEmailService.cs b/PlanTempus.Core/Email/IEmailService.cs similarity index 100% rename from Core/Email/IEmailService.cs rename to PlanTempus.Core/Email/IEmailService.cs diff --git a/Core/Email/PostmarkConfiguration.cs b/PlanTempus.Core/Email/PostmarkConfiguration.cs similarity index 100% rename from Core/Email/PostmarkConfiguration.cs rename to PlanTempus.Core/Email/PostmarkConfiguration.cs diff --git a/Core/Email/PostmarkEmailService.cs b/PlanTempus.Core/Email/PostmarkEmailService.cs similarity index 100% rename from Core/Email/PostmarkEmailService.cs rename to PlanTempus.Core/Email/PostmarkEmailService.cs diff --git a/Core/Entities/Accounts/Account.cs b/PlanTempus.Core/Entities/Accounts/Account.cs similarity index 100% rename from Core/Entities/Accounts/Account.cs rename to PlanTempus.Core/Entities/Accounts/Account.cs diff --git a/Core/Exceptions/ConfigurationException.cs b/PlanTempus.Core/Exceptions/ConfigurationException.cs similarity index 100% rename from Core/Exceptions/ConfigurationException.cs rename to PlanTempus.Core/Exceptions/ConfigurationException.cs diff --git a/Core/ISecureTokenizer.cs b/PlanTempus.Core/ISecureTokenizer.cs similarity index 100% rename from Core/ISecureTokenizer.cs rename to PlanTempus.Core/ISecureTokenizer.cs diff --git a/Core/ModuleRegistry/SecurityModule.cs b/PlanTempus.Core/ModuleRegistry/SecurityModule.cs similarity index 100% rename from Core/ModuleRegistry/SecurityModule.cs rename to PlanTempus.Core/ModuleRegistry/SecurityModule.cs diff --git a/Core/ModuleRegistry/SeqLoggingModule.cs b/PlanTempus.Core/ModuleRegistry/SeqLoggingModule.cs similarity index 100% rename from Core/ModuleRegistry/SeqLoggingModule.cs rename to PlanTempus.Core/ModuleRegistry/SeqLoggingModule.cs diff --git a/Core/ModuleRegistry/TelemetryModule.cs b/PlanTempus.Core/ModuleRegistry/TelemetryModule.cs similarity index 100% rename from Core/ModuleRegistry/TelemetryModule.cs rename to PlanTempus.Core/ModuleRegistry/TelemetryModule.cs diff --git a/Core/MultiKeyEncryption/MasterKey.cs b/PlanTempus.Core/MultiKeyEncryption/MasterKey.cs similarity index 100% rename from Core/MultiKeyEncryption/MasterKey.cs rename to PlanTempus.Core/MultiKeyEncryption/MasterKey.cs diff --git a/Core/MultiKeyEncryption/SecureConnectionString.cs b/PlanTempus.Core/MultiKeyEncryption/SecureConnectionString.cs similarity index 100% rename from Core/MultiKeyEncryption/SecureConnectionString.cs rename to PlanTempus.Core/MultiKeyEncryption/SecureConnectionString.cs diff --git a/Core/Outbox/IOutboxService.cs b/PlanTempus.Core/Outbox/IOutboxService.cs similarity index 100% rename from Core/Outbox/IOutboxService.cs rename to PlanTempus.Core/Outbox/IOutboxService.cs diff --git a/Core/Outbox/OutboxMessage.cs b/PlanTempus.Core/Outbox/OutboxMessage.cs similarity index 100% rename from Core/Outbox/OutboxMessage.cs rename to PlanTempus.Core/Outbox/OutboxMessage.cs diff --git a/Core/Outbox/OutboxModule.cs b/PlanTempus.Core/Outbox/OutboxModule.cs similarity index 100% rename from Core/Outbox/OutboxModule.cs rename to PlanTempus.Core/Outbox/OutboxModule.cs diff --git a/Core/Outbox/OutboxService.cs b/PlanTempus.Core/Outbox/OutboxService.cs similarity index 100% rename from Core/Outbox/OutboxService.cs rename to PlanTempus.Core/Outbox/OutboxService.cs diff --git a/Core/PlanTempus.Core.csproj b/PlanTempus.Core/PlanTempus.Core.csproj similarity index 100% rename from Core/PlanTempus.Core.csproj rename to PlanTempus.Core/PlanTempus.Core.csproj diff --git a/Core/SecureTokenizer.cs b/PlanTempus.Core/SecureTokenizer.cs similarity index 100% rename from Core/SecureTokenizer.cs rename to PlanTempus.Core/SecureTokenizer.cs diff --git a/PlanTempus.Core/SeqLogging/SeqBackgroundService.cs b/PlanTempus.Core/SeqLogging/SeqBackgroundService.cs new file mode 100644 index 0000000..ab0e9a9 --- /dev/null +++ b/PlanTempus.Core/SeqLogging/SeqBackgroundService.cs @@ -0,0 +1,87 @@ +using Microsoft.ApplicationInsights; +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.Extensions.Hosting; +using PlanTempus.Core.Telemetry; + +namespace PlanTempus.Core.SeqLogging +{ + public class SeqBackgroundService : BackgroundService + { + private readonly IMessageChannel _messageChannel; + private readonly TelemetryClient _telemetryClient; + private readonly SeqLogger _seqLogger; + private readonly TaskCompletionSource _shutdownComplete = new(); + + public SeqBackgroundService(TelemetryClient telemetryClient, + IMessageChannel messageChannel, + SeqLogger seqlogger) + { + _telemetryClient = telemetryClient; + _messageChannel = messageChannel; + _seqLogger = seqlogger; + + _telemetryClient.TrackTrace("SeqBackgroundService started"); + + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach (var telemetry in _messageChannel.Reader.ReadAllAsync()) + { + try + { + switch (telemetry) + { + case StopTelemetry: + StopGracefully(); + return; + + case ExceptionTelemetry et: + await _seqLogger.LogAsync(et); + break; + + case TraceTelemetry et: + await _seqLogger.LogAsync(et); + break; + + case DependencyTelemetry et: + await _seqLogger.LogAsync(et); + break; + + case RequestTelemetry et: + await _seqLogger.LogAsync(et); + break; + + case EventTelemetry et: + await _seqLogger.LogAsync(et); + break; + + default: + throw new NotSupportedException(telemetry.GetType().Name); + } + } + catch (Exception) + { + // Ignore errors processing telemetry + } + } + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + _messageChannel.Writer.TryWrite(new StopTelemetry()); + + // Vent max 10 sekunder på graceful shutdown + await Task.WhenAny(_shutdownComplete.Task, Task.Delay(10000)); + + await base.StopAsync(cancellationToken); + } + + private void StopGracefully() + { + _messageChannel.Dispose(); + _shutdownComplete.SetResult(); + } + } +} diff --git a/Core/SeqLogging/SeqConfiguration.cs b/PlanTempus.Core/SeqLogging/SeqConfiguration.cs similarity index 100% rename from Core/SeqLogging/SeqConfiguration.cs rename to PlanTempus.Core/SeqLogging/SeqConfiguration.cs diff --git a/Core/SeqLogging/SeqHttpClient.cs b/PlanTempus.Core/SeqLogging/SeqHttpClient.cs similarity index 100% rename from Core/SeqLogging/SeqHttpClient.cs rename to PlanTempus.Core/SeqLogging/SeqHttpClient.cs diff --git a/Core/SeqLogging/SeqLogger.cs b/PlanTempus.Core/SeqLogging/SeqLogger.cs similarity index 100% rename from Core/SeqLogging/SeqLogger.cs rename to PlanTempus.Core/SeqLogging/SeqLogger.cs diff --git a/Core/Telemetry/Enrichers/EnrichWithMetaTelemetry.cs b/PlanTempus.Core/Telemetry/Enrichers/EnrichWithMetaTelemetry.cs similarity index 100% rename from Core/Telemetry/Enrichers/EnrichWithMetaTelemetry.cs rename to PlanTempus.Core/Telemetry/Enrichers/EnrichWithMetaTelemetry.cs diff --git a/Core/Telemetry/IMessageChannel.cs b/PlanTempus.Core/Telemetry/IMessageChannel.cs similarity index 100% rename from Core/Telemetry/IMessageChannel.cs rename to PlanTempus.Core/Telemetry/IMessageChannel.cs diff --git a/Core/Telemetry/MessageChannel.cs b/PlanTempus.Core/Telemetry/MessageChannel.cs similarity index 100% rename from Core/Telemetry/MessageChannel.cs rename to PlanTempus.Core/Telemetry/MessageChannel.cs diff --git a/PlanTempus.Core/Telemetry/NotificationChannel.cs b/PlanTempus.Core/Telemetry/NotificationChannel.cs new file mode 100644 index 0000000..077e804 --- /dev/null +++ b/PlanTempus.Core/Telemetry/NotificationChannel.cs @@ -0,0 +1,13 @@ +using System.Threading.Channels; + +namespace PlanTempus.Core.Telemetry; + +public class NotificationChannel : IMessageChannel +{ + private readonly Channel _channel = Channel.CreateUnbounded(); + + public ChannelWriter Writer => _channel.Writer; + public ChannelReader Reader => _channel.Reader; + + public void Dispose() => _channel.Writer.Complete(); +} diff --git a/Core/Telemetry/SeqTelemetryChannel.cs b/PlanTempus.Core/Telemetry/SeqTelemetryChannel.cs similarity index 100% rename from Core/Telemetry/SeqTelemetryChannel.cs rename to PlanTempus.Core/Telemetry/SeqTelemetryChannel.cs diff --git a/PlanTempus.Core/Telemetry/StopTelemetry.cs b/PlanTempus.Core/Telemetry/StopTelemetry.cs new file mode 100644 index 0000000..966ef28 --- /dev/null +++ b/PlanTempus.Core/Telemetry/StopTelemetry.cs @@ -0,0 +1,25 @@ +using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.DataContracts; +using Microsoft.ApplicationInsights.Extensibility; + +namespace PlanTempus.Core.Telemetry; + +/// +/// Signal telemetry der bruges til at stoppe SeqBackgroundService gracefully. +/// Når denne læses fra channel, stopper servicen efter at have processeret alle tidligere beskeder. +/// +public class StopTelemetry : ITelemetry +{ + public DateTimeOffset Timestamp { get; set; } + public string Sequence { get; set; } + public TelemetryContext Context { get; } = new TelemetryContext(); + public IExtension Extension { get; set; } + + public ITelemetry DeepClone() => new StopTelemetry(); + + public void Sanitize() { } + + public void SerializeData(ISerializationWriter serializationWriter) { } + + +} diff --git a/Core/Telemetry/TelemetryExtensions.cs b/PlanTempus.Core/Telemetry/TelemetryExtensions.cs similarity index 100% rename from Core/Telemetry/TelemetryExtensions.cs rename to PlanTempus.Core/Telemetry/TelemetryExtensions.cs diff --git a/Database/Common/Validations.cs b/PlanTempus.Database/Common/Validations.cs similarity index 100% rename from Database/Common/Validations.cs rename to PlanTempus.Database/Common/Validations.cs diff --git a/Database/ConfigurationManagementSystem/SetupConfiguration.cs b/PlanTempus.Database/ConfigurationManagementSystem/SetupConfiguration.cs similarity index 100% rename from Database/ConfigurationManagementSystem/SetupConfiguration.cs rename to PlanTempus.Database/ConfigurationManagementSystem/SetupConfiguration.cs diff --git a/Database/Core/AccountService.cs b/PlanTempus.Database/Core/AccountService.cs similarity index 100% rename from Database/Core/AccountService.cs rename to PlanTempus.Database/Core/AccountService.cs diff --git a/Database/Core/DCL/SetupApplicationUser.cs b/PlanTempus.Database/Core/DCL/SetupApplicationUser.cs similarity index 100% rename from Database/Core/DCL/SetupApplicationUser.cs rename to PlanTempus.Database/Core/DCL/SetupApplicationUser.cs diff --git a/Database/Core/DCL/SetupDbAdmin.cs b/PlanTempus.Database/Core/DCL/SetupDbAdmin.cs similarity index 100% rename from Database/Core/DCL/SetupDbAdmin.cs rename to PlanTempus.Database/Core/DCL/SetupDbAdmin.cs diff --git a/Database/Core/DCL/SetupOrganizationUser.cs b/PlanTempus.Database/Core/DCL/SetupOrganizationUser.cs similarity index 100% rename from Database/Core/DCL/SetupOrganizationUser.cs rename to PlanTempus.Database/Core/DCL/SetupOrganizationUser.cs diff --git a/Database/Core/DDL/SetupIdentitySystem.cs b/PlanTempus.Database/Core/DDL/SetupIdentitySystem.cs similarity index 100% rename from Database/Core/DDL/SetupIdentitySystem.cs rename to PlanTempus.Database/Core/DDL/SetupIdentitySystem.cs diff --git a/Database/Core/DDL/SetupOutbox.cs b/PlanTempus.Database/Core/DDL/SetupOutbox.cs similarity index 100% rename from Database/Core/DDL/SetupOutbox.cs rename to PlanTempus.Database/Core/DDL/SetupOutbox.cs diff --git a/Database/Core/IDbConfigure.cs b/PlanTempus.Database/Core/IDbConfigure.cs similarity index 100% rename from Database/Core/IDbConfigure.cs rename to PlanTempus.Database/Core/IDbConfigure.cs diff --git a/Database/ModuleRegistry/DbPostgreSqlModule.cs b/PlanTempus.Database/ModuleRegistry/DbPostgreSqlModule.cs similarity index 100% rename from Database/ModuleRegistry/DbPostgreSqlModule.cs rename to PlanTempus.Database/ModuleRegistry/DbPostgreSqlModule.cs diff --git a/Database/NavigationSystem/Setup.cs b/PlanTempus.Database/NavigationSystem/Setup.cs similarity index 100% rename from Database/NavigationSystem/Setup.cs rename to PlanTempus.Database/NavigationSystem/Setup.cs diff --git a/Database/PlanTempus.Database.csproj b/PlanTempus.Database/PlanTempus.Database.csproj similarity index 77% rename from Database/PlanTempus.Database.csproj rename to PlanTempus.Database/PlanTempus.Database.csproj index 76de5e5..b76d642 100644 --- a/Database/PlanTempus.Database.csproj +++ b/PlanTempus.Database/PlanTempus.Database.csproj @@ -5,12 +5,12 @@ enable - - - - + + + + diff --git a/Database/RolesPermissionSystem/Setup.cs b/PlanTempus.Database/RolesPermissionSystem/Setup.cs similarity index 100% rename from Database/RolesPermissionSystem/Setup.cs rename to PlanTempus.Database/RolesPermissionSystem/Setup.cs diff --git a/Database/Tenants/InitializeTenantData.cs b/PlanTempus.Database/Tenants/InitializeTenantData.cs similarity index 100% rename from Database/Tenants/InitializeTenantData.cs rename to PlanTempus.Database/Tenants/InitializeTenantData.cs diff --git a/Application/Common/ComponentsViewLocationExpander.cs b/PlanTempus.DeprecatedApplication/Common/ComponentsViewLocationExpander.cs similarity index 100% rename from Application/Common/ComponentsViewLocationExpander.cs rename to PlanTempus.DeprecatedApplication/Common/ComponentsViewLocationExpander.cs diff --git a/Application/Components/ApiViewComponentBase.cs b/PlanTempus.DeprecatedApplication/Components/ApiViewComponentBase.cs similarity index 100% rename from Application/Components/ApiViewComponentBase.cs rename to PlanTempus.DeprecatedApplication/Components/ApiViewComponentBase.cs diff --git a/Application/Components/Navigation/Default.cshtml b/PlanTempus.DeprecatedApplication/Components/Navigation/Default.cshtml similarity index 100% rename from Application/Components/Navigation/Default.cshtml rename to PlanTempus.DeprecatedApplication/Components/Navigation/Default.cshtml diff --git a/Application/Components/Navigation/NavigationViewComponent.cs b/PlanTempus.DeprecatedApplication/Components/Navigation/NavigationViewComponent.cs similarity index 100% rename from Application/Components/Navigation/NavigationViewComponent.cs rename to PlanTempus.DeprecatedApplication/Components/Navigation/NavigationViewComponent.cs diff --git a/Application/Components/OrganizationViewComponent.cs b/PlanTempus.DeprecatedApplication/Components/OrganizationViewComponent.cs similarity index 100% rename from Application/Components/OrganizationViewComponent.cs rename to PlanTempus.DeprecatedApplication/Components/OrganizationViewComponent.cs diff --git a/Application/Pages/Index.cshtml b/PlanTempus.DeprecatedApplication/Pages/Index.cshtml similarity index 100% rename from Application/Pages/Index.cshtml rename to PlanTempus.DeprecatedApplication/Pages/Index.cshtml diff --git a/Application/Pages/Index.cshtml.cs b/PlanTempus.DeprecatedApplication/Pages/Index.cshtml.cs similarity index 100% rename from Application/Pages/Index.cshtml.cs rename to PlanTempus.DeprecatedApplication/Pages/Index.cshtml.cs diff --git a/Application/Pages/Shared/_Layout.cshtml b/PlanTempus.DeprecatedApplication/Pages/Shared/_Layout.cshtml similarity index 100% rename from Application/Pages/Shared/_Layout.cshtml rename to PlanTempus.DeprecatedApplication/Pages/Shared/_Layout.cshtml diff --git a/Application/Pages/Shared/_Layout.cshtml.css b/PlanTempus.DeprecatedApplication/Pages/Shared/_Layout.cshtml.css similarity index 100% rename from Application/Pages/Shared/_Layout.cshtml.css rename to PlanTempus.DeprecatedApplication/Pages/Shared/_Layout.cshtml.css diff --git a/Application/Pages/_ViewImports.cshtml b/PlanTempus.DeprecatedApplication/Pages/_ViewImports.cshtml similarity index 100% rename from Application/Pages/_ViewImports.cshtml rename to PlanTempus.DeprecatedApplication/Pages/_ViewImports.cshtml diff --git a/Application/Pages/_ViewStart.cshtml b/PlanTempus.DeprecatedApplication/Pages/_ViewStart.cshtml similarity index 100% rename from Application/Pages/_ViewStart.cshtml rename to PlanTempus.DeprecatedApplication/Pages/_ViewStart.cshtml diff --git a/Application/PlanTempus.Application.csproj b/PlanTempus.DeprecatedApplication/PlanTempus.DeprecatedApplication.csproj similarity index 100% rename from Application/PlanTempus.Application.csproj rename to PlanTempus.DeprecatedApplication/PlanTempus.DeprecatedApplication.csproj diff --git a/Application/Program.cs b/PlanTempus.DeprecatedApplication/Program.cs similarity index 100% rename from Application/Program.cs rename to PlanTempus.DeprecatedApplication/Program.cs diff --git a/Application/Properties/launchSettings.json b/PlanTempus.DeprecatedApplication/Properties/launchSettings.json similarity index 100% rename from Application/Properties/launchSettings.json rename to PlanTempus.DeprecatedApplication/Properties/launchSettings.json diff --git a/Application/Startup.cs b/PlanTempus.DeprecatedApplication/Startup.cs similarity index 100% rename from Application/Startup.cs rename to PlanTempus.DeprecatedApplication/Startup.cs diff --git a/Application/TypeScript/App.ts b/PlanTempus.DeprecatedApplication/TypeScript/App.ts similarity index 100% rename from Application/TypeScript/App.ts rename to PlanTempus.DeprecatedApplication/TypeScript/App.ts diff --git a/Application/TypeScript/Modules/esbuild_ReadMe.txt b/PlanTempus.DeprecatedApplication/TypeScript/Modules/esbuild_ReadMe.txt similarity index 100% rename from Application/TypeScript/Modules/esbuild_ReadMe.txt rename to PlanTempus.DeprecatedApplication/TypeScript/Modules/esbuild_ReadMe.txt diff --git a/Application/appconfiguration.Development.json b/PlanTempus.DeprecatedApplication/appconfiguration.Development.json similarity index 100% rename from Application/appconfiguration.Development.json rename to PlanTempus.DeprecatedApplication/appconfiguration.Development.json diff --git a/Application/appconfiguration.json b/PlanTempus.DeprecatedApplication/appconfiguration.json similarity index 100% rename from Application/appconfiguration.json rename to PlanTempus.DeprecatedApplication/appconfiguration.json diff --git a/Application/package-lock.json b/PlanTempus.DeprecatedApplication/package-lock.json similarity index 100% rename from Application/package-lock.json rename to PlanTempus.DeprecatedApplication/package-lock.json diff --git a/Application/package.json b/PlanTempus.DeprecatedApplication/package.json similarity index 100% rename from Application/package.json rename to PlanTempus.DeprecatedApplication/package.json diff --git a/Application/wwwroot/css/site.css b/PlanTempus.DeprecatedApplication/wwwroot/css/site.css similarity index 100% rename from Application/wwwroot/css/site.css rename to PlanTempus.DeprecatedApplication/wwwroot/css/site.css diff --git a/Application/wwwroot/favicon.ico b/PlanTempus.DeprecatedApplication/wwwroot/favicon.ico similarity index 100% rename from Application/wwwroot/favicon.ico rename to PlanTempus.DeprecatedApplication/wwwroot/favicon.ico diff --git a/Application/wwwroot/fonts/Rubik-Regular.woff2 b/PlanTempus.DeprecatedApplication/wwwroot/fonts/Rubik-Regular.woff2 similarity index 100% rename from Application/wwwroot/fonts/Rubik-Regular.woff2 rename to PlanTempus.DeprecatedApplication/wwwroot/fonts/Rubik-Regular.woff2 diff --git a/Application/wwwroot/fonts/Rubik-VariableFont_wght.woff2 b/PlanTempus.DeprecatedApplication/wwwroot/fonts/Rubik-VariableFont_wght.woff2 similarity index 100% rename from Application/wwwroot/fonts/Rubik-VariableFont_wght.woff2 rename to PlanTempus.DeprecatedApplication/wwwroot/fonts/Rubik-VariableFont_wght.woff2 diff --git a/Application/wwwroot/images/logo-1.png b/PlanTempus.DeprecatedApplication/wwwroot/images/logo-1.png similarity index 100% rename from Application/wwwroot/images/logo-1.png rename to PlanTempus.DeprecatedApplication/wwwroot/images/logo-1.png diff --git a/Application/wwwroot/images/pt-logo1.png b/PlanTempus.DeprecatedApplication/wwwroot/images/pt-logo1.png similarity index 100% rename from Application/wwwroot/images/pt-logo1.png rename to PlanTempus.DeprecatedApplication/wwwroot/images/pt-logo1.png diff --git a/Application/wwwroot/images/pt-logo2.png b/PlanTempus.DeprecatedApplication/wwwroot/images/pt-logo2.png similarity index 100% rename from Application/wwwroot/images/pt-logo2.png rename to PlanTempus.DeprecatedApplication/wwwroot/images/pt-logo2.png diff --git a/Application/wwwroot/js/app.js b/PlanTempus.DeprecatedApplication/wwwroot/js/app.js similarity index 100% rename from Application/wwwroot/js/app.js rename to PlanTempus.DeprecatedApplication/wwwroot/js/app.js diff --git a/Application/wwwroot/js/app.js.map b/PlanTempus.DeprecatedApplication/wwwroot/js/app.js.map similarity index 100% rename from Application/wwwroot/js/app.js.map rename to PlanTempus.DeprecatedApplication/wwwroot/js/app.js.map diff --git a/SetupInfrastructure/CreateRole.txt b/PlanTempus.SetupInfrastructure/CreateRole.txt similarity index 100% rename from SetupInfrastructure/CreateRole.txt rename to PlanTempus.SetupInfrastructure/CreateRole.txt diff --git a/SetupInfrastructure/PlanTempus.SetupInfrastructure.csproj b/PlanTempus.SetupInfrastructure/PlanTempus.SetupInfrastructure.csproj similarity index 84% rename from SetupInfrastructure/PlanTempus.SetupInfrastructure.csproj rename to PlanTempus.SetupInfrastructure/PlanTempus.SetupInfrastructure.csproj index 5c9236b..d1f423e 100644 --- a/SetupInfrastructure/PlanTempus.SetupInfrastructure.csproj +++ b/PlanTempus.SetupInfrastructure/PlanTempus.SetupInfrastructure.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/SetupInfrastructure/Program.cs b/PlanTempus.SetupInfrastructure/Program.cs similarity index 100% rename from SetupInfrastructure/Program.cs rename to PlanTempus.SetupInfrastructure/Program.cs diff --git a/SetupInfrastructure/Startup.cs b/PlanTempus.SetupInfrastructure/Startup.cs similarity index 100% rename from SetupInfrastructure/Startup.cs rename to PlanTempus.SetupInfrastructure/Startup.cs diff --git a/SetupInfrastructure/appconfiguration.json b/PlanTempus.SetupInfrastructure/appconfiguration.json similarity index 100% rename from SetupInfrastructure/appconfiguration.json rename to PlanTempus.SetupInfrastructure/appconfiguration.json diff --git a/SqlManagement/.dbeaver/.credentials-config.json.bak b/PlanTempus.SqlManagement/.dbeaver/.credentials-config.json.bak similarity index 100% rename from SqlManagement/.dbeaver/.credentials-config.json.bak rename to PlanTempus.SqlManagement/.dbeaver/.credentials-config.json.bak diff --git a/SqlManagement/.dbeaver/.data-sources.json.bak b/PlanTempus.SqlManagement/.dbeaver/.data-sources.json.bak similarity index 100% rename from SqlManagement/.dbeaver/.data-sources.json.bak rename to PlanTempus.SqlManagement/.dbeaver/.data-sources.json.bak diff --git a/SqlManagement/.dbeaver/.project-metadata.json.bak b/PlanTempus.SqlManagement/.dbeaver/.project-metadata.json.bak similarity index 100% rename from SqlManagement/.dbeaver/.project-metadata.json.bak rename to PlanTempus.SqlManagement/.dbeaver/.project-metadata.json.bak diff --git a/SqlManagement/.dbeaver/credentials-config.json b/PlanTempus.SqlManagement/.dbeaver/credentials-config.json similarity index 100% rename from SqlManagement/.dbeaver/credentials-config.json rename to PlanTempus.SqlManagement/.dbeaver/credentials-config.json diff --git a/SqlManagement/.dbeaver/data-sources.json b/PlanTempus.SqlManagement/.dbeaver/data-sources.json similarity index 100% rename from SqlManagement/.dbeaver/data-sources.json rename to PlanTempus.SqlManagement/.dbeaver/data-sources.json diff --git a/SqlManagement/.dbeaver/project-metadata.json b/PlanTempus.SqlManagement/.dbeaver/project-metadata.json similarity index 100% rename from SqlManagement/.dbeaver/project-metadata.json rename to PlanTempus.SqlManagement/.dbeaver/project-metadata.json diff --git a/SqlManagement/.dbeaver/project-settings.json b/PlanTempus.SqlManagement/.dbeaver/project-settings.json similarity index 100% rename from SqlManagement/.dbeaver/project-settings.json rename to PlanTempus.SqlManagement/.dbeaver/project-settings.json diff --git a/SqlManagement/.project b/PlanTempus.SqlManagement/.project similarity index 100% rename from SqlManagement/.project rename to PlanTempus.SqlManagement/.project diff --git a/SqlManagement/Dashboards/Test.dashboard b/PlanTempus.SqlManagement/Dashboards/Test.dashboard similarity index 100% rename from SqlManagement/Dashboards/Test.dashboard rename to PlanTempus.SqlManagement/Dashboards/Test.dashboard diff --git a/SqlManagement/Diagrams/global.erd b/PlanTempus.SqlManagement/Diagrams/global.erd similarity index 100% rename from SqlManagement/Diagrams/global.erd rename to PlanTempus.SqlManagement/Diagrams/global.erd diff --git a/SqlManagement/Scripts/Script.sql b/PlanTempus.SqlManagement/Scripts/Script.sql similarity index 100% rename from SqlManagement/Scripts/Script.sql rename to PlanTempus.SqlManagement/Scripts/Script.sql diff --git a/SqlManagement/Scripts/SmartConfigSystem.sql b/PlanTempus.SqlManagement/Scripts/SmartConfigSystem.sql similarity index 100% rename from SqlManagement/Scripts/SmartConfigSystem.sql rename to PlanTempus.SqlManagement/Scripts/SmartConfigSystem.sql diff --git a/SqlManagement/Scripts/grant-privileges.sql b/PlanTempus.SqlManagement/Scripts/grant-privileges.sql similarity index 100% rename from SqlManagement/Scripts/grant-privileges.sql rename to PlanTempus.SqlManagement/Scripts/grant-privileges.sql diff --git a/Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs b/PlanTempus.Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs similarity index 100% rename from Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs rename to PlanTempus.Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs diff --git a/Tests/CodeSnippets/sandbox.sql b/PlanTempus.Tests/CodeSnippets/sandbox.sql similarity index 100% rename from Tests/CodeSnippets/sandbox.sql rename to PlanTempus.Tests/CodeSnippets/sandbox.sql diff --git a/Tests/CommandQueryHandlerTests/HandlerTest.cs b/PlanTempus.Tests/CommandQueryHandlerTests/HandlerTest.cs similarity index 100% rename from Tests/CommandQueryHandlerTests/HandlerTest.cs rename to PlanTempus.Tests/CommandQueryHandlerTests/HandlerTest.cs diff --git a/Tests/CommandQueryHandlerTests/ResponseTests.cs b/PlanTempus.Tests/CommandQueryHandlerTests/ResponseTests.cs similarity index 100% rename from Tests/CommandQueryHandlerTests/ResponseTests.cs rename to PlanTempus.Tests/CommandQueryHandlerTests/ResponseTests.cs diff --git a/Tests/ConfigurationSystem/SetupConfigurationTests.cs b/PlanTempus.Tests/ConfigurationSystem/SetupConfigurationTests.cs similarity index 100% rename from Tests/ConfigurationSystem/SetupConfigurationTests.cs rename to PlanTempus.Tests/ConfigurationSystem/SetupConfigurationTests.cs diff --git a/Tests/ConfigurationTests/JsonConfigurationProviderTests.cs b/PlanTempus.Tests/ConfigurationTests/JsonConfigurationProviderTests.cs similarity index 100% rename from Tests/ConfigurationTests/JsonConfigurationProviderTests.cs rename to PlanTempus.Tests/ConfigurationTests/JsonConfigurationProviderTests.cs diff --git a/Tests/ConfigurationTests/KeyValueJsonHandlingTests.cs b/PlanTempus.Tests/ConfigurationTests/KeyValueJsonHandlingTests.cs similarity index 100% rename from Tests/ConfigurationTests/KeyValueJsonHandlingTests.cs rename to PlanTempus.Tests/ConfigurationTests/KeyValueJsonHandlingTests.cs diff --git a/Tests/ConfigurationTests/SmartConfigProviderTests.cs b/PlanTempus.Tests/ConfigurationTests/SmartConfigProviderTests.cs similarity index 100% rename from Tests/ConfigurationTests/SmartConfigProviderTests.cs rename to PlanTempus.Tests/ConfigurationTests/SmartConfigProviderTests.cs diff --git a/Tests/ConfigurationTests/appconfiguration.dev.json b/PlanTempus.Tests/ConfigurationTests/appconfiguration.dev.json similarity index 100% rename from Tests/ConfigurationTests/appconfiguration.dev.json rename to PlanTempus.Tests/ConfigurationTests/appconfiguration.dev.json diff --git a/Tests/Logging/SeqBackgroundServiceTest.cs b/PlanTempus.Tests/Logging/SeqBackgroundServiceTest.cs similarity index 100% rename from Tests/Logging/SeqBackgroundServiceTest.cs rename to PlanTempus.Tests/Logging/SeqBackgroundServiceTest.cs diff --git a/Tests/Logging/SeqLoggerTests.cs b/PlanTempus.Tests/Logging/SeqLoggerTests.cs similarity index 100% rename from Tests/Logging/SeqLoggerTests.cs rename to PlanTempus.Tests/Logging/SeqLoggerTests.cs diff --git a/Tests/Logging/SeqTelemetryChannelTest.cs b/PlanTempus.Tests/Logging/SeqTelemetryChannelTest.cs similarity index 100% rename from Tests/Logging/SeqTelemetryChannelTest.cs rename to PlanTempus.Tests/Logging/SeqTelemetryChannelTest.cs diff --git a/Tests/PasswordHasherTest.cs b/PlanTempus.Tests/PasswordHasherTest.cs similarity index 100% rename from Tests/PasswordHasherTest.cs rename to PlanTempus.Tests/PasswordHasherTest.cs diff --git a/Tests/PlanTempus.X.TDD.csproj b/PlanTempus.Tests/PlanTempus.X.TDD.csproj similarity index 77% rename from Tests/PlanTempus.X.TDD.csproj rename to PlanTempus.Tests/PlanTempus.X.TDD.csproj index f411132..0b14a69 100644 --- a/Tests/PlanTempus.X.TDD.csproj +++ b/PlanTempus.Tests/PlanTempus.X.TDD.csproj @@ -9,23 +9,23 @@ - - - - - - - + + + + + + + - - + + - + diff --git a/Tests/PostgresTests.cs b/PlanTempus.Tests/PostgresTests.cs similarity index 100% rename from Tests/PostgresTests.cs rename to PlanTempus.Tests/PostgresTests.cs diff --git a/Tests/SecureConnectionStringTests.cs b/PlanTempus.Tests/SecureConnectionStringTests.cs similarity index 100% rename from Tests/SecureConnectionStringTests.cs rename to PlanTempus.Tests/SecureConnectionStringTests.cs diff --git a/Tests/TestFixture.cs b/PlanTempus.Tests/TestFixture.cs similarity index 100% rename from Tests/TestFixture.cs rename to PlanTempus.Tests/TestFixture.cs diff --git a/Tests/appconfiguration.dev.json b/PlanTempus.Tests/appconfiguration.dev.json similarity index 100% rename from Tests/appconfiguration.dev.json rename to PlanTempus.Tests/appconfiguration.dev.json diff --git a/PlanTempus.X.BDD/BddTestFixture.cs b/PlanTempus.X.BDD/BddTestFixture.cs index f8f7b3a..ed39b91 100644 --- a/PlanTempus.X.BDD/BddTestFixture.cs +++ b/PlanTempus.X.BDD/BddTestFixture.cs @@ -11,6 +11,7 @@ using PlanTempus.Core.Email; using PlanTempus.Core.ModuleRegistry; using PlanTempus.Core.Outbox; using PlanTempus.Core.SeqLogging; +using PlanTempus.Core.Telemetry; using PlanTempus.Database.ModuleRegistry; using CrypticWizard.RandomWordGenerator; @@ -84,6 +85,7 @@ public abstract class BddTestFixture : FeatureFixture PostmarkConfiguration = configuration.GetSection("Postmark").ToObject() }); + builder.RegisterType().As>().SingleInstance(); builder.RegisterType().SingleInstance(); builder.RegisterType().SingleInstance(); @@ -104,8 +106,11 @@ public abstract class BddTestFixture : FeatureFixture [TestCleanup] public void CleanupContainer() { + // 1. Stop OutboxListener først (så der ikke genereres mere telemetry) _cts?.Cancel(); _outboxListener?.StopAsync(CancellationToken.None).Wait(); + + // 2. Stop SeqBackgroundService - den sender stop signal, dræner channel og lukker ned _seqBackgroundService?.StopAsync(CancellationToken.None).Wait(); Trace.Flush(); @@ -121,7 +126,7 @@ public abstract class BddTestFixture : FeatureFixture } catch (System.Threading.Channels.ChannelClosedException) { - // Channel already closed by SeqBackgroundService.StopAsync + // Channel already closed } Container = null; diff --git a/PlanTempus.X.BDD/PlanTempus.X.BDD.csproj b/PlanTempus.X.BDD/PlanTempus.X.BDD.csproj index a3190fc..0b4b97d 100644 --- a/PlanTempus.X.BDD/PlanTempus.X.BDD.csproj +++ b/PlanTempus.X.BDD/PlanTempus.X.BDD.csproj @@ -22,10 +22,10 @@ - - - + + + diff --git a/PlanTempus.sln b/PlanTempus.sln index e82f7ea..6a0a8f2 100644 --- a/PlanTempus.sln +++ b/PlanTempus.sln @@ -2,15 +2,13 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.10.35013.160 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.Core", "Core\PlanTempus.Core.csproj", "{7B554252-1CE4-44BD-B108-B0BDCCB24742}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.Core", "PlanTempus.Core\PlanTempus.Core.csproj", "{7B554252-1CE4-44BD-B108-B0BDCCB24742}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.X.TDD", "Tests\PlanTempus.X.TDD.csproj", "{85614050-CFB0-4E39-81D3-7D99946449D9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.X.TDD", "PlanTempus.Tests\PlanTempus.X.TDD.csproj", "{85614050-CFB0-4E39-81D3-7D99946449D9}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.Application", "Application\PlanTempus.Application.csproj", "{111CE8AE-E637-4376-A5A3-88D33E77EA88}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.Database", "PlanTempus.Database\PlanTempus.Database.csproj", "{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.Database", "Database\PlanTempus.Database.csproj", "{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.SetupInfrastructure", "SetupInfrastructure\PlanTempus.SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlanTempus.SetupInfrastructure", "PlanTempus.SetupInfrastructure\PlanTempus.SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanTempus.X.BDD", "PlanTempus.X.BDD\PlanTempus.X.BDD.csproj", "{8CA2246B-7D8C-40DA-9042-CA17A7A7672B}" EndProject @@ -18,6 +16,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanTempus.Components", "Pl EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPostgresql", "TestPostgresLISTEN\TestPostgresql.csproj", "{67C167C4-8086-0556-39DA-5F9DF6CEE51F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlanTempus.Application", "PlanTempus.Application\PlanTempus.Application.csproj", "{7B846B69-FB5B-799B-02A7-4F1AADC6F7C9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,10 +32,6 @@ Global {85614050-CFB0-4E39-81D3-7D99946449D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {85614050-CFB0-4E39-81D3-7D99946449D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {85614050-CFB0-4E39-81D3-7D99946449D9}.Release|Any CPU.Build.0 = Release|Any CPU - {111CE8AE-E637-4376-A5A3-88D33E77EA88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {111CE8AE-E637-4376-A5A3-88D33E77EA88}.Debug|Any CPU.Build.0 = Debug|Any CPU - {111CE8AE-E637-4376-A5A3-88D33E77EA88}.Release|Any CPU.ActiveCfg = Release|Any CPU - {111CE8AE-E637-4376-A5A3-88D33E77EA88}.Release|Any CPU.Build.0 = Release|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -56,6 +52,10 @@ Global {67C167C4-8086-0556-39DA-5F9DF6CEE51F}.Debug|Any CPU.Build.0 = Debug|Any CPU {67C167C4-8086-0556-39DA-5F9DF6CEE51F}.Release|Any CPU.ActiveCfg = Release|Any CPU {67C167C4-8086-0556-39DA-5F9DF6CEE51F}.Release|Any CPU.Build.0 = Release|Any CPU + {7B846B69-FB5B-799B-02A7-4F1AADC6F7C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B846B69-FB5B-799B-02A7-4F1AADC6F7C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B846B69-FB5B-799B-02A7-4F1AADC6F7C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B846B69-FB5B-799B-02A7-4F1AADC6F7C9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE