diff --git a/app/CalendarServer.csproj b/app/CalendarServer.csproj deleted file mode 100644 index 9aa2a08..0000000 --- a/app/CalendarServer.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - net8.0 - enable - enable - CalendarServer - - - \ No newline at end of file diff --git a/app/Controllers/HomeController.cs b/app/Controllers/HomeController.cs deleted file mode 100644 index efc173f..0000000 --- a/app/Controllers/HomeController.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace CalendarServer.Controllers; - -public class HomeController : Controller -{ - public IActionResult Index() - { - ViewData["Title"] = "Dashboard"; - return View(); - } -} diff --git a/app/Features/Language/Models/SupportedCulture.cs b/app/Features/Language/Models/SupportedCulture.cs deleted file mode 100644 index 3525c5e..0000000 --- a/app/Features/Language/Models/SupportedCulture.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace CalendarServer.Features.Language.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/app/Features/Language/Services/ILocalizationService.cs b/app/Features/Language/Services/ILocalizationService.cs deleted file mode 100644 index 430e50e..0000000 --- a/app/Features/Language/Services/ILocalizationService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using CalendarServer.Features.Language.Models; - -namespace CalendarServer.Features.Language.Services; - -public interface ILocalizationService -{ - string Get(string key, string? culture = null); - string CurrentCulture { get; } - IEnumerable GetSupportedCultures(); -} diff --git a/app/Features/Language/Services/JsonLocalizationService.cs b/app/Features/Language/Services/JsonLocalizationService.cs deleted file mode 100644 index 7b90bca..0000000 --- a/app/Features/Language/Services/JsonLocalizationService.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Text.Json; -using CalendarServer.Features.Language.Models; - -namespace CalendarServer.Features.Language.Services; - -public class JsonLocalizationService : ILocalizationService -{ - private readonly string _translationsPath; - - public JsonLocalizationService(IWebHostEnvironment env) - { - _translationsPath = Path.Combine(env.ContentRootPath, "Features", "Language", "Translations"); - } - - public string CurrentCulture => "en"; - - 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/app/Features/Language/TagHelpers/LocalizeTagHelper.cs b/app/Features/Language/TagHelpers/LocalizeTagHelper.cs deleted file mode 100644 index 73f0fe9..0000000 --- a/app/Features/Language/TagHelpers/LocalizeTagHelper.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Razor.TagHelpers; -using CalendarServer.Features.Language.Services; - -namespace CalendarServer.Features.Language.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/app/Features/Language/Translations/da.json b/app/Features/Language/Translations/da.json deleted file mode 100644 index c569ae6..0000000 --- a/app/Features/Language/Translations/da.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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/app/Features/Language/Translations/en.json b/app/Features/Language/Translations/en.json deleted file mode 100644 index a66a7b7..0000000 --- a/app/Features/Language/Translations/en.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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/app/Features/Menu/Models/MenuGroup.cs b/app/Features/Menu/Models/MenuGroup.cs deleted file mode 100644 index f2f3eb0..0000000 --- a/app/Features/Menu/Models/MenuGroup.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace CalendarServer.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/app/Features/Menu/Models/MenuItem.cs b/app/Features/Menu/Models/MenuItem.cs deleted file mode 100644 index 04e8172..0000000 --- a/app/Features/Menu/Models/MenuItem.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace CalendarServer.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/app/Features/Menu/Models/UserRole.cs b/app/Features/Menu/Models/UserRole.cs deleted file mode 100644 index 1adc056..0000000 --- a/app/Features/Menu/Models/UserRole.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace CalendarServer.Features.Menu.Models; - -/// -/// User roles for menu visibility. Higher value = more access. -/// -public enum UserRole -{ - Staff = 0, - Manager = 1, - Admin = 2 -} diff --git a/app/Features/Menu/Services/IMenuService.cs b/app/Features/Menu/Services/IMenuService.cs deleted file mode 100644 index 3cbccb5..0000000 --- a/app/Features/Menu/Services/IMenuService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using CalendarServer.Features.Menu.Models; - -namespace CalendarServer.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/app/Features/Menu/Services/MockMenuService.cs b/app/Features/Menu/Services/MockMenuService.cs deleted file mode 100644 index 4b90217..0000000 --- a/app/Features/Menu/Services/MockMenuService.cs +++ /dev/null @@ -1,187 +0,0 @@ -using CalendarServer.Features.Menu.Models; -using CalendarServer.Features.Language.Services; - -namespace CalendarServer.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/app/Features/Menu/SideMenuViewComponent.cs b/app/Features/Menu/SideMenuViewComponent.cs deleted file mode 100644 index a14de55..0000000 --- a/app/Features/Menu/SideMenuViewComponent.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using CalendarServer.Features.Menu.Models; -using CalendarServer.Features.Menu.Services; - -namespace CalendarServer.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/app/Features/Menu/SideMenuViewModel.cs b/app/Features/Menu/SideMenuViewModel.cs deleted file mode 100644 index 84cef10..0000000 --- a/app/Features/Menu/SideMenuViewModel.cs +++ /dev/null @@ -1,12 +0,0 @@ -using CalendarServer.Features.Menu.Models; - -namespace CalendarServer.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/app/Program.cs b/app/Program.cs deleted file mode 100644 index 53c699f..0000000 --- a/app/Program.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using CalendarServer.Features.Menu.Services; -using CalendarServer.Features.Language.Services; - -var builder = WebApplication.CreateBuilder(args); - -// Add MVC services -builder.Services.AddControllersWithViews(); - -// Register application services -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -var app = builder.Build(); - -// Serve static files from wwwroot -app.UseStaticFiles(); - -// Configure routing -app.UseRouting(); - -// Map MVC routes -app.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - -app.Run("http://localhost:8000"); \ No newline at end of file diff --git a/app/Views/Home/Index.cshtml b/app/Views/Home/Index.cshtml deleted file mode 100644 index 714eaec..0000000 --- a/app/Views/Home/Index.cshtml +++ /dev/null @@ -1,91 +0,0 @@ -@{ - 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/app/Views/Shared/Components/SideMenu/Default.cshtml b/app/Views/Shared/Components/SideMenu/Default.cshtml deleted file mode 100644 index 12087b5..0000000 --- a/app/Views/Shared/Components/SideMenu/Default.cshtml +++ /dev/null @@ -1,39 +0,0 @@ -@model CalendarServer.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/app/Views/Shared/Components/_ProfileDrawer.cshtml b/app/Views/Shared/Components/_ProfileDrawer.cshtml deleted file mode 100644 index 9e42620..0000000 --- a/app/Views/Shared/Components/_ProfileDrawer.cshtml +++ /dev/null @@ -1,49 +0,0 @@ - - - Profil - - - - - - - - MJ - Maria Jensen - maria@salon.dk - - - - - - - - Min profil - - - - Indstillinger - - - - - - - - - Mørk tilstand - - - - - - - - - - - - Log ud - - - diff --git a/app/Views/Shared/Components/_SideMenu.cshtml b/app/Views/Shared/Components/_SideMenu.cshtml deleted file mode 100644 index 0e0d210..0000000 --- a/app/Views/Shared/Components/_SideMenu.cshtml +++ /dev/null @@ -1,78 +0,0 @@ - - - - Salon OS - - - - - - - - - Dashboard - - - Dashboard - - - - Kalender - - - - Kasse - - - - - - Data - - - Produkter & Lager - - - - Leverandører - - - - Kunder - - - - Medarbejdere - - - - - - Analyse - - - Statistik & Rapporter - - - - - - System - - - Indstillinger - - - - Abonnement & Konto - - - - - - - - Lås skærm - - - diff --git a/app/Views/Shared/Components/_TopBar.cshtml b/app/Views/Shared/Components/_TopBar.cshtml deleted file mode 100644 index 41b140a..0000000 --- a/app/Views/Shared/Components/_TopBar.cshtml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - ⌘K - - - - - - - 3 - - - - - - - MJ - - Maria Jensen - Administrator - - - - diff --git a/app/Views/Shared/_Layout.cshtml b/app/Views/Shared/_Layout.cshtml deleted file mode 100644 index 337b9c7..0000000 --- a/app/Views/Shared/_Layout.cshtml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - @ViewData["Title"] - Salon OS - - - - - - - - - - - - - - @await RenderSectionAsync("Styles", required: false) - - - - @await Component.InvokeAsync("SideMenu") - @await Html.PartialAsync("Components/_TopBar") - - - @RenderBody() - - - - @await Html.PartialAsync("Components/_ProfileDrawer") - - - - @await RenderSectionAsync("Scripts", required: false) - - diff --git a/app/Views/_ViewImports.cshtml b/app/Views/_ViewImports.cshtml deleted file mode 100644 index 584d299..0000000 --- a/app/Views/_ViewImports.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@using CalendarServer -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, CalendarServer diff --git a/app/Views/_ViewStart.cshtml b/app/Views/_ViewStart.cshtml deleted file mode 100644 index 820a2f6..0000000 --- a/app/Views/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "_Layout"; -} diff --git a/app/build.js b/app/build.js deleted file mode 100644 index 4aa944e..0000000 --- a/app/build.js +++ /dev/null @@ -1,24 +0,0 @@ -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/app/wwwroot/css/app-layout.css b/app/wwwroot/css/app-layout.css deleted file mode 100644 index 2f57b74..0000000 --- a/app/wwwroot/css/app-layout.css +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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/app/wwwroot/css/base.css b/app/wwwroot/css/base.css deleted file mode 100644 index 00669dc..0000000 --- a/app/wwwroot/css/base.css +++ /dev/null @@ -1,118 +0,0 @@ -/** - * 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/app/wwwroot/css/design-system.css b/app/wwwroot/css/design-system.css deleted file mode 100644 index beb61ec..0000000 --- a/app/wwwroot/css/design-system.css +++ /dev/null @@ -1,163 +0,0 @@ -/** - * 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/app/wwwroot/css/drawers.css b/app/wwwroot/css/drawers.css deleted file mode 100644 index 2805da5..0000000 --- a/app/wwwroot/css/drawers.css +++ /dev/null @@ -1,258 +0,0 @@ -/** - * 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/app/wwwroot/css/page.css b/app/wwwroot/css/page.css deleted file mode 100644 index cf4b9c7..0000000 --- a/app/wwwroot/css/page.css +++ /dev/null @@ -1,204 +0,0 @@ -/** - * 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/app/wwwroot/css/sidebar.css b/app/wwwroot/css/sidebar.css deleted file mode 100644 index cc9354e..0000000 --- a/app/wwwroot/css/sidebar.css +++ /dev/null @@ -1,246 +0,0 @@ -/** - * 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/app/wwwroot/css/stats.css b/app/wwwroot/css/stats.css deleted file mode 100644 index 2d50a5f..0000000 --- a/app/wwwroot/css/stats.css +++ /dev/null @@ -1,258 +0,0 @@ -/** - * 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/app/wwwroot/css/topbar.css b/app/wwwroot/css/topbar.css deleted file mode 100644 index 6c9d182..0000000 --- a/app/wwwroot/css/topbar.css +++ /dev/null @@ -1,180 +0,0 @@ -/** - * 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/app/wwwroot/fonts/Poppins-Black.woff b/app/wwwroot/fonts/Poppins-Black.woff deleted file mode 100644 index 65f3801..0000000 Binary files a/app/wwwroot/fonts/Poppins-Black.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-BlackItalic.woff b/app/wwwroot/fonts/Poppins-BlackItalic.woff deleted file mode 100644 index 63eb02e..0000000 Binary files a/app/wwwroot/fonts/Poppins-BlackItalic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-Bold.woff b/app/wwwroot/fonts/Poppins-Bold.woff deleted file mode 100644 index 28fe6b6..0000000 Binary files a/app/wwwroot/fonts/Poppins-Bold.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-BoldItalic.woff b/app/wwwroot/fonts/Poppins-BoldItalic.woff deleted file mode 100644 index 8ec438d..0000000 Binary files a/app/wwwroot/fonts/Poppins-BoldItalic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-ExtraBold.woff b/app/wwwroot/fonts/Poppins-ExtraBold.woff deleted file mode 100644 index 10df1ac..0000000 Binary files a/app/wwwroot/fonts/Poppins-ExtraBold.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-ExtraBoldItalic.woff b/app/wwwroot/fonts/Poppins-ExtraBoldItalic.woff deleted file mode 100644 index 38090a0..0000000 Binary files a/app/wwwroot/fonts/Poppins-ExtraBoldItalic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-ExtraLight.woff b/app/wwwroot/fonts/Poppins-ExtraLight.woff deleted file mode 100644 index 8eb4095..0000000 Binary files a/app/wwwroot/fonts/Poppins-ExtraLight.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-ExtraLightItalic.woff b/app/wwwroot/fonts/Poppins-ExtraLightItalic.woff deleted file mode 100644 index 560f65e..0000000 Binary files a/app/wwwroot/fonts/Poppins-ExtraLightItalic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-Italic.woff b/app/wwwroot/fonts/Poppins-Italic.woff deleted file mode 100644 index 341cbb3..0000000 Binary files a/app/wwwroot/fonts/Poppins-Italic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-Light.woff b/app/wwwroot/fonts/Poppins-Light.woff deleted file mode 100644 index 61f6a5c..0000000 Binary files a/app/wwwroot/fonts/Poppins-Light.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-LightItalic.woff b/app/wwwroot/fonts/Poppins-LightItalic.woff deleted file mode 100644 index 2706cd4..0000000 Binary files a/app/wwwroot/fonts/Poppins-LightItalic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-Medium.woff b/app/wwwroot/fonts/Poppins-Medium.woff deleted file mode 100644 index e2b717d..0000000 Binary files a/app/wwwroot/fonts/Poppins-Medium.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-MediumItalic.woff b/app/wwwroot/fonts/Poppins-MediumItalic.woff deleted file mode 100644 index d8101a7..0000000 Binary files a/app/wwwroot/fonts/Poppins-MediumItalic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-Regular.woff b/app/wwwroot/fonts/Poppins-Regular.woff deleted file mode 100644 index 609eb3d..0000000 Binary files a/app/wwwroot/fonts/Poppins-Regular.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-SemiBold.woff b/app/wwwroot/fonts/Poppins-SemiBold.woff deleted file mode 100644 index c097e14..0000000 Binary files a/app/wwwroot/fonts/Poppins-SemiBold.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-SemiBoldItalic.woff b/app/wwwroot/fonts/Poppins-SemiBoldItalic.woff deleted file mode 100644 index 902dcfd..0000000 Binary files a/app/wwwroot/fonts/Poppins-SemiBoldItalic.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-Thin.woff b/app/wwwroot/fonts/Poppins-Thin.woff deleted file mode 100644 index d21fbd3..0000000 Binary files a/app/wwwroot/fonts/Poppins-Thin.woff and /dev/null differ diff --git a/app/wwwroot/fonts/Poppins-ThinItalic.woff b/app/wwwroot/fonts/Poppins-ThinItalic.woff deleted file mode 100644 index ea3a42f..0000000 Binary files a/app/wwwroot/fonts/Poppins-ThinItalic.woff and /dev/null differ diff --git a/app/wwwroot/ts/app.ts b/app/wwwroot/ts/app.ts deleted file mode 100644 index 16735f5..0000000 --- a/app/wwwroot/ts/app.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 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/app/wwwroot/ts/modules/drawers.ts b/app/wwwroot/ts/modules/drawers.ts deleted file mode 100644 index 7ee36ad..0000000 --- a/app/wwwroot/ts/modules/drawers.ts +++ /dev/null @@ -1,226 +0,0 @@ -/** - * 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/app/wwwroot/ts/modules/lockscreen.ts b/app/wwwroot/ts/modules/lockscreen.ts deleted file mode 100644 index f6a5878..0000000 --- a/app/wwwroot/ts/modules/lockscreen.ts +++ /dev/null @@ -1,182 +0,0 @@ -/** - * 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/app/wwwroot/ts/modules/search.ts b/app/wwwroot/ts/modules/search.ts deleted file mode 100644 index d6eac21..0000000 --- a/app/wwwroot/ts/modules/search.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * 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/app/wwwroot/ts/modules/sidebar.ts b/app/wwwroot/ts/modules/sidebar.ts deleted file mode 100644 index ddb0a2b..0000000 --- a/app/wwwroot/ts/modules/sidebar.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * 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/app/wwwroot/ts/modules/theme.ts b/app/wwwroot/ts/modules/theme.ts deleted file mode 100644 index b14e4b5..0000000 --- a/app/wwwroot/ts/modules/theme.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * 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/app/wwwroot/ts/tsconfig.json b/app/wwwroot/ts/tsconfig.json deleted file mode 100644 index 87d8c61..0000000 --- a/app/wwwroot/ts/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "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/wwwroot/plantempus-sales.html b/wwwroot/plantempus-sales.html new file mode 100644 index 0000000..9439cd8 --- /dev/null +++ b/wwwroot/plantempus-sales.html @@ -0,0 +1,1896 @@ + + + + + + + Plantempus - Din salon digitaliseret og optimeret + + + + + + + + + + +
+
+
+ + Til professionelle frisører og skønhedssaloner +
+

Mere tid til det du er god til

+

+ Plantempus tager sig af booking, administration og papirarbejde - så du kan fokusere på dine kunder og dit håndværk. +

+ +
+
+ + +
+
+
10t
+
Mere tid til kunder om ugen
+
+
+
0 kr
+
I tabte bookinger
+
+
+
24/7
+
Kunderne kan selv booke
+
+
+
5 min
+
At lukke kassen dagligt
+
+
+ + +
+
+
+
For professionelle frisører
+

Gør det du elsker - ikke papirarbejde

+
+ +
+

+ Du blev frisør for at klippe hår - ikke for at sidde med papirarbejde. + Plantempus håndterer booking, påmindelser og løn automatisk. + Kunderne booker selv online. Du får tid til det du er god til. +

+

+ 10 timer mere om ugen til dine kunder. + Ingen dobbeltbookinger. Ingen glemte påmindelser. Ingen timer ved Excel. + Bare en fyldt kalender og tilfredse kunder. +

+

+ Gratis i 14 dage. + Ingen binding. Intet kreditkort. +

+
+
+
+ + +
+
+
+
Kernefunktionalitet
+

Alt hvad din salon har brug for - i ét system

+

+ Fra online booking til lønudbetaling. Plantempus dækker hele din forretning. +

+
+ +
+
+
+ +
+

Booking der kører sig selv

+

+ Kunderne booker selv online når det passer dem - også klokken 22 om aftenen. + Systemet sender selv påmindelser, så du slipper for no-shows. +

+
+ +
+
+ +
+

Husk hver kunde

+

+ Gem farveformler, noter og præferencer direkte i systemet. + Næste gang kunden kommer, har du alt ved hånden - selv om det er 6 måneder siden. +

+
+ +
+
+ +
+

Nem betaling

+

+ Tag imod kort, kontant, MobilePay og gavekort - alt sammen i samme system. + Scan stregkoder på produkter og de kommer automatisk med på kvitteringen. +

+
+ +
+
+ +
+

Styr på medarbejdere

+

+ Planlæg vagter, hold styr på ferie og sygedage. + Løn og provision bliver beregnet automatisk - eksporter direkte til dit lønsystem. +

+
+ +
+
+ +
+

Nemt kassearbejde

+

+ Luk kassen på 5 minutter i stedet for 30. + Systemet tæller op for dig og viser hvor meget der skal være i skuffen. +

+
+ +
+
+ +
+

Se hvordan det går

+

+ Simpelt dashboard der viser hvad du tjener, hvilke kunder der kommer tilbage, + og hvor meget hver medarbejder omsætter for. Ingen komplicerede rapporter. +

+
+
+
+
+ + +
+
+
+
+
+ + Smart hjælp +
+

Systemet arbejder for dig - ikke omvendt

+

+ Plantempus gør det tunge arbejde. Det foreslår de bedste tider til dine kunder (så de ikke ringer midt i en klipning), + giver dig besked når du har huller i kalenderen der kan fyldes, og husker hvilke produkter dine kunder plejer at købe. + Det er som at have en ekstra medarbejder - der aldrig har fri. +

+
+
+
10t
+
Mere tid til kunder/uge
+
+
+
0
+
Glemte påmindelser
+
+
+
+8
+
Ekstra kunder/uge
+
+
+
+
+ AI Dashboard +
+
+
+
+ + +
+
+
+
Alt du har brug for
+

Ét system der klarer det hele

+

+ Fra booking til løn. Fra kundejournal til kasseafstemning. Alt samlet ét sted. +

+
+ +
+ +
+
+
+ +
+

Booking & Kalender

+
+
    +
  • Kunderne booker selv online
  • +
  • Forslag til ledige tider
  • +
  • Venteliste når I er fyldt op
  • +
  • Flyt bookinger med træk-og-slip
  • +
  • Book flere medarbejdere til samme kunde
  • +
  • Automatiske påmindelser
  • +
+
+ + +
+
+
+ +
+

Kunde-management

+
+
    +
  • Alle kunder ét sted
  • +
  • Marker VIP-kunder og stamkunder
  • +
  • Gem noter og observationer
  • +
  • Farveformler der følger med
  • +
  • Link familie-medlemmer
  • +
  • Håndter marketing-tilladelser
  • +
+
+ + +
+
+
+ +
+

Medarbejdere

+
+
    +
  • Planlæg vagter for hele ugen
  • +
  • Løn regnes automatisk ud
  • +
  • Gem kontrakter og dokumenter
  • +
  • Hold styr på ferie og sygdom
  • +
  • Påmindelser om certificeringer
  • +
  • Se hvem der tjener mest
  • +
+
+ + +
+
+
+ +
+

Økonomi & Kasse

+
+
    +
  • Tag imod kort, kontant, MobilePay
  • +
  • Sælg og administrer gavekort
  • +
  • Kasseafstemning på 5 minutter
  • +
  • Del betalingen på flere metoder
  • +
  • Print eller email kvitteringer
  • +
  • Scan produkter ind
  • +
+
+ + +
+
+
+ +
+

Smart hjælp

+
+
    +
  • Foreslår de bedste booking-tider
  • +
  • Finder huller i din kalender
  • +
  • Advarer når kunder er ved at stoppe
  • +
  • Anbefaler produkter til kunder
  • +
  • Viser hvad der sælger bedst
  • +
  • Giver dig overblik hver dag
  • +
+
+ + +
+
+
+ +
+

Ting der sker selv

+
+
    +
  • SMS går automatisk ud
  • +
  • Emails når noget ændres
  • +
  • Send løn direkte til revisor
  • +
  • Kvitteringer til sygeforsikring
  • +
  • Synk med din telefon-kalender
  • +
  • Kvitteringer printes eller sendes
  • +
+
+ + +
+
+
+ +
+

Virker med det du har

+
+
    +
  • Intect, Proløn, Zenegy og flere
  • +
  • Sender til "danmark" sygeforsikring
  • +
  • Tæl besøgende på hjemmesiden
  • +
  • Beskyt kundernes privatliv
  • +
  • Synk med Google/Apple kalender
  • +
  • Kan kobles til andre systemer
  • +
+
+ + +
+
+
+ +
+

Gør det til dit eget

+
+
    +
  • Slå funktioner til og fra
  • +
  • Dit logo og dine farver
  • +
  • Skriv dine egne beskeder
  • +
  • Sæt åbningstider og helligdage
  • +
  • Flere saloner i samme system
  • +
  • Mørkt tema til aftenvagter
  • +
+
+
+
+
+ + +
+
+
+
Resultater
+

Det virker i praksis

+

+ Tal fra saloner der allerede bruger Plantempus +

+
+ +
+
+
10t
+
Mere tid til kunder hver uge
+
+
+
95%
+
Kunder møder op (ingen no-shows)
+
+
+
8
+
Ekstra kunder om ugen
+
+
+
0 kr
+
Tabte bookinger om måneden
+
+
+
+
+ + +
+
+
+
Fordele
+

Hvorfor vælge Plantempus?

+
+ +
+
+
+ +
+
+

Start i dag

+

+ Intet at installere. Ingen kompliceret opsætning. + Du kan være klar til at modtage bookinger samme dag du starter. +

+
+
+ +
+
+ +
+
+

Ingen overraskelser

+

+ Fast pris hver måned. Ingen skjulte gebyrer. Ubegrænsede bookinger og SMS inkluderet. + Du betaler kun for antallet af medarbejdere. +

+
+
+ +
+
+ +
+
+

Sikkerhed & GDPR

+

+ Dine data er krypteret og hostet sikkert i EU. Fuld GDPR-compliance, + automatisk backup og 99.9% uptime garanti. +

+
+
+ +
+
+ +
+
+

Hjælp når du har brug for det

+

+ Skriv eller ring på dansk til folk der kender til frisørbranchen. + Vi svarer hurtigt og forklarer tingene i et sprog du forstår. +

+
+
+ +
+
+ +
+
+

Virker på mobilen

+

+ Kunderne booker fra deres telefon. Du kan tjekke kalenderen og ændre bookinger fra din tablet. + Intet kræver at du sidder ved en computer. +

+
+
+ +
+
+ +
+
+

Bliver bedre hele tiden

+

+ Nye funktioner tilføjes løbende uden du skal betale mere. + Vi lytter til hvad I har brug for og bygger det ind. +

+
+
+
+
+
+ + +
+
+
+
Priser
+

Vælg den plan der passer til dig

+

+ Alle planer inkluderer 14 dages gratis prøveperiode. Ingen binding. Opsig når som helst. +

+
+ +
+ +
+

Basis

+

1-3 brugere

+
+ 299 + kr/md +
+
    +
  • Op til 3 brugere
  • +
  • Online booking
  • +
  • Kalender & aftalestyring
  • +
  • Kundekartotek
  • +
  • SMS-påmindelser
  • +
  • Email-notifikationer
  • +
  • Basis rapporter
  • +
  • Email support
  • +
+ +
+ + + + + +
+

Enterprise

+

8+ brugere

+
+ Kontakt os +
+
    +
  • Ubegrænset brugere
  • +
  • Alt fra Pro
  • +
  • Flere lokationer
  • +
  • Tilpasset integration
  • +
  • Dedikeret kontaktperson
  • +
  • SLA & uptime garanti
  • +
  • On-premise option
  • +
  • 24/7 prioriteret support
  • +
+ +
+
+
+
+ + +
+
+
+
Sammenligning
+

Plantempus vs. Traditionelle Systemer

+

+ Se forskellen på et system bygget til moderne saloner +

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeaturePlantempusAndre systemer
Avanceret kundesøgning
Detaljerede statistikker med grafer
Farveformler & håranalyser
Automatisk lønberegning med provision
Multi-payment checkout (flere metoder)
Kasseafstemning
Stregkodescanner med produkt-genkendelse
Smart tidsforslag til kunder
Moderne, hurtig grænseflade
Eksport til 5+ lønsystemer
+
+ ✓ = Inkluderet · △ = Begrænset · — = Ikke tilgængelig +
+
+
+
+ + +
+
+
+
Se det rigtige system
+

Ikke bare infographics - det her er rigtigt

+

+ Vi er stolte af vores UX og performance. Se hvordan systemet faktisk ser ud. +

+
+ +
+
+

+ + Dashboard med rigtige data +

+

+ Se dagens omsætning, bookinger og medarbejder-status på ét blik. Ikke bare tal - visuelle grafer der giver mening. +

+ + Se live demo + +
+ +
+

+ + Kunde-søgning der virker +

+

+ Find kunder på navn, telefon, email - med det samme. Filtrér på VIP, stamkunder eller nye kunder. Hurtigt og præcist. +

+ + Se live demo + +
+ +
+

+ + Kundeprofiler med detaljer +

+

+ Farveformler, håranalyser, præferencer og købs-historie. Alt organiseret og nemt at finde. +

+ + Se live demo + +
+ +
+

+ + Løn & Provision +

+

+ Se præcis hvad hver medarbejder har tjent. Eksportér direkte til Intect, Proløn eller Zenegy med et klik. +

+ + Se live demo + +
+
+ +
+

+ + Alle demos er interaktive - klik og prøv dem +

+

+ Ikke mock-ups. Ikke static billeder. Det her er det rigtige system i aktion. +

+
+
+
+ + +
+
+
+
Performance & Design
+

Bygget med omtanke - mærkes i hverdagen

+
+ +
+
+
+ +
+
+

Lynhurtigt

+

+ Under 1 sekund load-tid. Ingen langsomme overgang + +er. + Systemet reagerer med det samme - som det skal være. +

+
+
+ +
+
+ +
+
+

Gennemtænkt UX

+

+ Hver knap, hver farve, hver animation er testet. + Designet så det er intuitivt - også for den der ikke er teknisk. +

+
+
+ +
+
+ +
+
+

Statistik der giver mening

+

+ Ikke bare tal på en side. Visuelle grafer, trends og indsigter. + Se med det samme hvad der virker og hvad der ikke gør. +

+
+
+ +
+
+ +
+
+

Moderne design

+

+ Ikke et system fra 2010. Rent, moderne interface der matcher + det professionelle udtryk din salon har. +

+
+
+
+
+
+ + +
+
+
+
Kundeudtalelser
+

Hvad siger vores brugere?

+
+ +
+
+
+ "Nu har jeg faktisk tid til at snakke med mine kunder i stedet for at rende efter telefonen. + Bookinger sker automatisk, påmindelser går selv ud, og jeg kan se hele ugen med et blik. Det er befriende." +
+
+
KK
+
+
Karina Knudsen
+
Ejer, KARINA KNUDSEN®
+
+
+
+ +
+
+ "Mine kunder elsker at de kan booke online klokken 22 om aftenen når de lige kommer i tanke om det. + Og jeg slipper for at svare på telefonen hele dagen. Win-win." +
+
+
MH
+
+
Maria Hansen
+
Salonchef, Salon Bellezza
+
+
+
+ +
+
+ "Før brugte jeg en hel formiddag hver måned på løn. Nu tager det 10 minutter. + Systemet har styr på hvem der har arbejdet hvornår, og sender det hele til min revisor." +
+
+
AS
+
+
Anna Sørensen
+
Økonomiansvarlig, Beauty Studio
+
+
+
+
+
+
+ + +
+
+

Klar til at optimere din salon?

+

Start din gratis 14-dages prøveperiode i dag. Ingen kreditkort påkrævet.

+ +
+
+ + + + + + + + +
  • Om os \ No newline at end of file diff --git a/wwwroot/poc-indstillinger.html b/wwwroot/poc-indstillinger.html index 2654706..107979b 100644 --- a/wwwroot/poc-indstillinger.html +++ b/wwwroot/poc-indstillinger.html @@ -3064,6 +3064,72 @@ Tak for din handel! + + + + + + + + AI Kundeanalyse + Forstå dine kunders adfærd og forbliv proaktiv. AI'en analyserer bookingmønstre, forudser hvornår kunder har brug for en tid, og identificerer kunder der er ved at falde fra. + + + + Til + Fra + + + + + + + Booking-prediktion baseret på historik + + + + Churn-detektion – se hvem der er ved at falde fra + + + + Service-præference analyse pr. kunde + + + + Sæson-korrektion i forudsigelser + + + + Win-back kampagner for inaktive kunder + + + + Automatisk personlig beskedgenerering + + + + + -34% + Færre tabte kunder + + + +18% + Genbookinger + + + 3.8x + ROI på kampagner + + + + + +79 kr/md + Beta + + Prøv gratis i 14 dage + + +