Adds comprehensive customers list and management components

Introduces customer-related view components for table and row display
Implements mock data loading and customer list rendering
Adds localization support for customer-related text
Enhances UI with detailed customer information and interactions
This commit is contained in:
Janus C. H. Knudsen 2026-01-21 18:00:53 +01:00
parent cd7acaf490
commit 6ef001e35f
11 changed files with 869 additions and 675 deletions

View file

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.Customers.Components;
public class CustomerRowViewComponent : ViewComponent
{
public IViewComponentResult Invoke(CustomerItemViewModel customer)
{
return View(customer);
}
}

View file

@ -0,0 +1,38 @@
@model PlanTempus.Application.Features.Customers.Components.CustomerItemViewModel
<swp-data-table-row data-name="@Model.FullName" data-visits="@Model.Visits" data-href="/kunder/@Model.Id">
<swp-data-table-cell>
<swp-avatar class="@Model.AvatarColor">@Model.Initials</swp-avatar>
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
<span>@Model.FullName</span>
</swp-data-table-cell>
<swp-data-table-cell>@Model.Phone</swp-data-table-cell>
<swp-data-table-cell>@Model.Email</swp-data-table-cell>
<swp-data-table-cell>@Model.Visits</swp-data-table-cell>
<swp-data-table-cell>@Model.LastVisit</swp-data-table-cell>
<swp-data-table-cell>@Model.PreferredHairdresser</swp-data-table-cell>
<swp-data-table-cell>@Model.CreatedAt</swp-data-table-cell>
<swp-data-table-cell>
@foreach (var tag in Model.Tags)
{
<swp-tag class="@tag">@GetTagLabel(tag)</swp-tag>
}
</swp-data-table-cell>
</swp-data-table-row>
@functions {
string GetTagLabel(string tag)
{
return tag switch
{
"vip" => "VIP",
"ny" => "Ny",
"allergi" => "Allergi",
"sensitiv" => "Sensitiv",
"stamkunde" => "Stamkunde",
_ => tag
};
}
}

View file

@ -0,0 +1,139 @@
using System.Globalization;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using PlanTempus.Application.Features.Localization.Services;
namespace PlanTempus.Application.Features.Customers.Components;
public class CustomerTableViewComponent : ViewComponent
{
private readonly ILocalizationService _localization;
private readonly IWebHostEnvironment _env;
public CustomerTableViewComponent(ILocalizationService localization, IWebHostEnvironment env)
{
_localization = localization;
_env = env;
}
public IViewComponentResult Invoke()
{
var data = LoadCustomerData();
var model = new CustomerTableViewModel
{
SearchPlaceholder = _localization.Get("customers.searchPlaceholder"),
ExportButtonText = _localization.Get("customers.export"),
CreateButtonText = _localization.Get("customers.create"),
ColumnName = _localization.Get("customers.column.name"),
ColumnPhone = _localization.Get("customers.column.phone"),
ColumnEmail = _localization.Get("customers.column.email"),
ColumnVisits = _localization.Get("customers.column.visits"),
ColumnLastVisit = _localization.Get("customers.column.lastVisit"),
ColumnHairdresser = _localization.Get("customers.column.hairdresser"),
ColumnCreated = _localization.Get("customers.column.created"),
ColumnTags = _localization.Get("customers.column.tags"),
EmptySearchText = _localization.Get("customers.emptySearch"),
Customers = data.Customers
.OrderBy(c => c.FirstName)
.ThenBy(c => c.LastName)
.Select(c => new CustomerItemViewModel
{
Id = c.Id,
FullName = $"{c.FirstName} {c.LastName}",
Initials = c.Initials,
Phone = c.Phone,
Email = c.Email,
Visits = c.Visits,
LastVisit = FormatLastVisit(c.LastVisit),
PreferredHairdresser = c.PreferredHairdresser,
CreatedAt = FormatCreatedAt(c.CreatedAt),
Tags = c.Tags,
AvatarColor = c.AvatarColor
})
.ToList()
};
return View(model);
}
private CustomerMockData LoadCustomerData()
{
var jsonPath = Path.Combine(_env.ContentRootPath, "Features", "Customers", "Data", "customersMock.json");
var json = System.IO.File.ReadAllText(jsonPath);
return JsonSerializer.Deserialize<CustomerMockData>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new CustomerMockData();
}
private static string FormatLastVisit(string dateStr)
{
if (DateTime.TryParse(dateStr, out var date))
{
return date.ToString("d. MMM", new CultureInfo("da-DK")).TrimEnd('.');
}
return dateStr;
}
private static string FormatCreatedAt(string dateStr)
{
if (DateTime.TryParse(dateStr, out var date))
{
return date.ToString("MMM yyyy", new CultureInfo("da-DK"));
}
return dateStr;
}
}
public class CustomerTableViewModel
{
public required string SearchPlaceholder { get; init; }
public required string ExportButtonText { get; init; }
public required string CreateButtonText { get; init; }
public required string ColumnName { get; init; }
public required string ColumnPhone { get; init; }
public required string ColumnEmail { get; init; }
public required string ColumnVisits { get; init; }
public required string ColumnLastVisit { get; init; }
public required string ColumnHairdresser { get; init; }
public required string ColumnCreated { get; init; }
public required string ColumnTags { get; init; }
public required string EmptySearchText { get; init; }
public required IReadOnlyList<CustomerItemViewModel> Customers { get; init; }
}
public class CustomerItemViewModel
{
public required string Id { get; init; }
public required string FullName { get; init; }
public required string Initials { get; init; }
public required string Phone { get; init; }
public required string Email { get; init; }
public int Visits { get; init; }
public required string LastVisit { get; init; }
public required string PreferredHairdresser { get; init; }
public required string CreatedAt { get; init; }
public required IReadOnlyList<string> Tags { get; init; }
public string? AvatarColor { get; init; }
}
internal class CustomerMockData
{
public List<CustomerData> Customers { get; set; } = new();
}
internal class CustomerData
{
public string Id { get; set; } = "";
public string FirstName { get; set; } = "";
public string LastName { get; set; } = "";
public string Initials { get; set; } = "";
public string Phone { get; set; } = "";
public string Email { get; set; } = "";
public int Visits { get; set; }
public string LastVisit { get; set; } = "";
public string PreferredHairdresser { get; set; } = "";
public string CreatedAt { get; set; } = "";
public List<string> Tags { get; set; } = new();
public string? AvatarColor { get; set; }
}

View file

@ -0,0 +1,42 @@
@model PlanTempus.Application.Features.Customers.Components.CustomerTableViewModel
<swp-action-bar>
<swp-search-input>
<i class="ph ph-magnifying-glass"></i>
<input type="text" id="searchInput" placeholder="@Model.SearchPlaceholder" />
</swp-search-input>
<swp-btn-group>
<swp-btn class="secondary">
<i class="ph ph-export"></i>
<span>@Model.ExportButtonText</span>
</swp-btn>
<swp-btn class="primary">
<i class="ph ph-plus"></i>
<span>@Model.CreateButtonText</span>
</swp-btn>
</swp-btn-group>
</swp-action-bar>
<swp-card class="customers-list">
<swp-data-table>
<swp-data-table-header>
<swp-data-table-cell>@Model.ColumnName</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnPhone</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnEmail</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnVisits</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnLastVisit</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnHairdresser</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnCreated</swp-data-table-cell>
<swp-data-table-cell>@Model.ColumnTags</swp-data-table-cell>
</swp-data-table-header>
@foreach (var customer in Model.Customers)
{
@await Component.InvokeAsync("CustomerRow", customer)
}
</swp-data-table>
<swp-empty-state id="emptyState" style="display: none;">
<i class="ph ph-users"></i>
<span>@Model.EmptySearchText</span>
</swp-empty-state>
</swp-card>

View file

@ -0,0 +1,172 @@
{
"customers": [
{
"id": "anna-jensen",
"firstName": "Anna",
"lastName": "Jensen",
"initials": "AJ",
"phone": "+45 22 33 44 55",
"email": "anna.j@hotmail.dk",
"visits": 6,
"lastVisit": "2025-11-15",
"preferredHairdresser": "Nina K.",
"createdAt": "2024-09-01",
"tags": [],
"avatarColor": null
},
{
"id": "camilla-holm",
"firstName": "Camilla",
"lastName": "Holm",
"initials": "CH",
"phone": "+45 66 77 88 99",
"email": "camilla.h@outlook.dk",
"visits": 25,
"lastVisit": "2025-10-28",
"preferredHairdresser": "Emma L.",
"createdAt": "2022-12-01",
"tags": ["vip"],
"avatarColor": null
},
{
"id": "emma-larsen",
"firstName": "Emma",
"lastName": "Larsen",
"initials": "EL",
"phone": "+45 12 34 56 78",
"email": "emma.l@gmail.com",
"visits": 8,
"lastVisit": "2025-12-05",
"preferredHairdresser": "Nina K.",
"createdAt": "2024-06-01",
"tags": [],
"avatarColor": null
},
{
"id": "freja-christensen",
"firstName": "Freja",
"lastName": "Christensen",
"initials": "FC",
"phone": "+45 55 66 77 88",
"email": "freja.c@outlook.dk",
"visits": 31,
"lastVisit": "2025-11-20",
"preferredHairdresser": "Emma L.",
"createdAt": "2022-08-01",
"tags": ["vip", "allergi"],
"avatarColor": null
},
{
"id": "ida-andersen",
"firstName": "Ida",
"lastName": "Andersen",
"initials": "IA",
"phone": "+45 11 22 33 44",
"email": "ida@firma.dk",
"visits": 3,
"lastVisit": "2025-11-28",
"preferredHairdresser": "Sofie M.",
"createdAt": "2025-10-01",
"tags": ["ny"],
"avatarColor": null
},
{
"id": "katrine-berg",
"firstName": "Katrine",
"lastName": "Berg",
"initials": "KB",
"phone": "+45 55 66 77 88",
"email": "katrine.b@firma.dk",
"visits": 12,
"lastVisit": "2025-11-01",
"preferredHairdresser": "Nina K.",
"createdAt": "2024-04-01",
"tags": [],
"avatarColor": null
},
{
"id": "line-frost",
"firstName": "Line",
"lastName": "Frost",
"initials": "LF",
"phone": "+45 88 99 00 11",
"email": "line.f@mail.dk",
"visits": 9,
"lastVisit": "2025-10-15",
"preferredHairdresser": "Nina K.",
"createdAt": "2024-05-01",
"tags": ["sensitiv"],
"avatarColor": null
},
{
"id": "louise-hansen",
"firstName": "Louise",
"lastName": "Hansen",
"initials": "LH",
"phone": "+45 33 44 55 66",
"email": "louise.h@gmail.com",
"visits": 18,
"lastVisit": "2025-11-10",
"preferredHairdresser": "Emma L.",
"createdAt": "2023-02-01",
"tags": ["stamkunde"],
"avatarColor": "purple"
},
{
"id": "maja-petersen",
"firstName": "Maja",
"lastName": "Petersen",
"initials": "MP",
"phone": "+45 98 76 54 32",
"email": "maja.p@mail.dk",
"visits": 22,
"lastVisit": "2025-12-01",
"preferredHairdresser": "Emma L.",
"createdAt": "2023-01-01",
"tags": ["stamkunde"],
"avatarColor": "blue"
},
{
"id": "maria-olsen",
"firstName": "Maria",
"lastName": "Olsen",
"initials": "MO",
"phone": "+45 44 55 66 77",
"email": "maria.o@mail.dk",
"visits": 2,
"lastVisit": "2025-11-05",
"preferredHairdresser": "Sofie M.",
"createdAt": "2025-11-01",
"tags": ["ny"],
"avatarColor": "amber"
},
{
"id": "rikke-skov",
"firstName": "Rikke",
"lastName": "Skov",
"initials": "RS",
"phone": "+45 77 88 99 00",
"email": "rikke.s@gmail.com",
"visits": 4,
"lastVisit": "2025-10-20",
"preferredHairdresser": "Sofie M.",
"createdAt": "2025-08-01",
"tags": [],
"avatarColor": null
},
{
"id": "sofie-nielsen",
"firstName": "Sofie",
"lastName": "Nielsen",
"initials": "SN",
"phone": "+45 23 45 67 89",
"email": "sofie@email.dk",
"visits": 14,
"lastVisit": "2025-12-09",
"preferredHairdresser": "Emma L.",
"createdAt": "2024-03-01",
"tags": ["vip"],
"avatarColor": null
}
]
}

View file

@ -31,288 +31,7 @@
</swp-sticky-header>
<swp-page-container>
<swp-action-bar>
<swp-search-input>
<i class="ph ph-magnifying-glass"></i>
<input type="text" id="searchInput" placeholder="Søg kunde (navn, telefon, email...)" />
</swp-search-input>
<swp-btn-group>
<swp-btn class="secondary">
<i class="ph ph-export"></i>
<span localize="customers.export">Eksporter</span>
</swp-btn>
<swp-btn class="primary">
<i class="ph ph-plus"></i>
<span localize="customers.create">Ny kunde</span>
</swp-btn>
</swp-btn-group>
</swp-action-bar>
<swp-card class="customers-list">
<swp-data-table>
<swp-data-table-header>
<swp-data-table-cell></swp-data-table-cell>
<swp-data-table-cell localize="customers.column.name">Navn</swp-data-table-cell>
<swp-data-table-cell localize="customers.column.phone">Telefon</swp-data-table-cell>
<swp-data-table-cell localize="customers.column.email">Email</swp-data-table-cell>
<swp-data-table-cell localize="customers.column.visits">Besøg</swp-data-table-cell>
<swp-data-table-cell localize="customers.column.lastVisit">Sidste</swp-data-table-cell>
<swp-data-table-cell localize="customers.column.hairdresser">Frisør</swp-data-table-cell>
<swp-data-table-cell localize="customers.column.created">Oprettet</swp-data-table-cell>
<swp-data-table-cell localize="customers.column.tags">Tags</swp-data-table-cell>
</swp-data-table-header>
<swp-data-table-row data-name="Anna Jensen" data-visits="6" data-created="2024-09" data-href="/kunder/anna-jensen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>AJ</swp-avatar>
<span>Anna Jensen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 22 33 44 55</swp-data-table-cell>
<swp-data-table-cell>anna.j@hotmail.dk</swp-data-table-cell>
<swp-data-table-cell>6</swp-data-table-cell>
<swp-data-table-cell>15. nov</swp-data-table-cell>
<swp-data-table-cell>Nina K.</swp-data-table-cell>
<swp-data-table-cell>Sep 2024</swp-data-table-cell>
<swp-data-table-cell></swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Camilla Holm" data-visits="25" data-created="2022-12" data-tags="vip" data-href="/kunder/camilla-holm">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>CH</swp-avatar>
<span>Camilla Holm</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 66 77 88 99</swp-data-table-cell>
<swp-data-table-cell>camilla.h@outlook.dk</swp-data-table-cell>
<swp-data-table-cell>25</swp-data-table-cell>
<swp-data-table-cell>28. okt</swp-data-table-cell>
<swp-data-table-cell>Emma L.</swp-data-table-cell>
<swp-data-table-cell>Dec 2022</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="vip">VIP</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Emma Larsen" data-visits="8" data-created="2024-06" data-href="/kunder/emma-larsen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>EL</swp-avatar>
<span>Emma Larsen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 12 34 56 78</swp-data-table-cell>
<swp-data-table-cell>emma.l@gmail.com</swp-data-table-cell>
<swp-data-table-cell>8</swp-data-table-cell>
<swp-data-table-cell>5. dec</swp-data-table-cell>
<swp-data-table-cell>Nina K.</swp-data-table-cell>
<swp-data-table-cell>Jun 2024</swp-data-table-cell>
<swp-data-table-cell></swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Freja Christensen" data-visits="31" data-created="2022-08" data-tags="vip,allergi" data-href="/kunder/freja-christensen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>FC</swp-avatar>
<span>Freja Christensen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 55 66 77 88</swp-data-table-cell>
<swp-data-table-cell>freja.c@outlook.dk</swp-data-table-cell>
<swp-data-table-cell>31</swp-data-table-cell>
<swp-data-table-cell>20. nov</swp-data-table-cell>
<swp-data-table-cell>Emma L.</swp-data-table-cell>
<swp-data-table-cell>Aug 2022</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="vip">VIP</swp-tag>
<swp-tag class="allergi">Allergi</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Ida Andersen" data-visits="3" data-created="2025-10" data-tags="ny" data-href="/kunder/ida-andersen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>IA</swp-avatar>
<span>Ida Andersen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 11 22 33 44</swp-data-table-cell>
<swp-data-table-cell>ida@firma.dk</swp-data-table-cell>
<swp-data-table-cell>3</swp-data-table-cell>
<swp-data-table-cell>28. nov</swp-data-table-cell>
<swp-data-table-cell>Sofie M.</swp-data-table-cell>
<swp-data-table-cell>Okt 2025</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="ny">Ny</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Katrine Berg" data-visits="12" data-created="2024-04" data-href="/kunder/katrine-berg">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>KB</swp-avatar>
<span>Katrine Berg</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 55 66 77 88</swp-data-table-cell>
<swp-data-table-cell>katrine.b@firma.dk</swp-data-table-cell>
<swp-data-table-cell>12</swp-data-table-cell>
<swp-data-table-cell>1. nov</swp-data-table-cell>
<swp-data-table-cell>Nina K.</swp-data-table-cell>
<swp-data-table-cell>Apr 2024</swp-data-table-cell>
<swp-data-table-cell></swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Line Frost" data-visits="9" data-created="2024-05" data-tags="sensitiv" data-href="/kunder/line-frost">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>LF</swp-avatar>
<span>Line Frost</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 88 99 00 11</swp-data-table-cell>
<swp-data-table-cell>line.f@mail.dk</swp-data-table-cell>
<swp-data-table-cell>9</swp-data-table-cell>
<swp-data-table-cell>15. okt</swp-data-table-cell>
<swp-data-table-cell>Nina K.</swp-data-table-cell>
<swp-data-table-cell>Maj 2024</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="sensitiv">Sensitiv</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Louise Hansen" data-visits="18" data-created="2023-02" data-tags="stamkunde" data-href="/kunder/louise-hansen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar class="purple">LH</swp-avatar>
<span>Louise Hansen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 33 44 55 66</swp-data-table-cell>
<swp-data-table-cell>louise.h@gmail.com</swp-data-table-cell>
<swp-data-table-cell>18</swp-data-table-cell>
<swp-data-table-cell>10. nov</swp-data-table-cell>
<swp-data-table-cell>Emma L.</swp-data-table-cell>
<swp-data-table-cell>Feb 2023</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="stamkunde">Stamkunde</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Maja Petersen" data-visits="22" data-created="2023-01" data-tags="stamkunde" data-href="/kunder/maja-petersen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar class="blue">MP</swp-avatar>
<span>Maja Petersen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 98 76 54 32</swp-data-table-cell>
<swp-data-table-cell>maja.p@mail.dk</swp-data-table-cell>
<swp-data-table-cell>22</swp-data-table-cell>
<swp-data-table-cell>1. dec</swp-data-table-cell>
<swp-data-table-cell>Emma L.</swp-data-table-cell>
<swp-data-table-cell>Jan 2023</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="stamkunde">Stamkunde</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Maria Olsen" data-visits="2" data-created="2025-11" data-tags="ny" data-href="/kunder/maria-olsen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar class="amber">MO</swp-avatar>
<span>Maria Olsen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 44 55 66 77</swp-data-table-cell>
<swp-data-table-cell>maria.o@mail.dk</swp-data-table-cell>
<swp-data-table-cell>2</swp-data-table-cell>
<swp-data-table-cell>5. nov</swp-data-table-cell>
<swp-data-table-cell>Sofie M.</swp-data-table-cell>
<swp-data-table-cell>Nov 2025</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="ny">Ny</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Rikke Skov" data-visits="4" data-created="2025-08" data-href="/kunder/rikke-skov">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>RS</swp-avatar>
<span>Rikke Skov</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 77 88 99 00</swp-data-table-cell>
<swp-data-table-cell>rikke.s@gmail.com</swp-data-table-cell>
<swp-data-table-cell>4</swp-data-table-cell>
<swp-data-table-cell>20. okt</swp-data-table-cell>
<swp-data-table-cell>Sofie M.</swp-data-table-cell>
<swp-data-table-cell>Aug 2025</swp-data-table-cell>
<swp-data-table-cell></swp-data-table-cell>
</swp-data-table-row>
<swp-data-table-row data-name="Sofie Nielsen" data-visits="14" data-created="2024-03" data-tags="vip" data-href="/kunder/sofie-nielsen">
<swp-data-table-cell class="quick-view">
<swp-quick-view-btn data-drawer-trigger="customer-drawer">
<i class="ph ph-sidebar"></i>
</swp-quick-view-btn>
</swp-data-table-cell>
<swp-data-table-cell>
<swp-avatar>SN</swp-avatar>
<span>Sofie Nielsen</span>
</swp-data-table-cell>
<swp-data-table-cell>+45 23 45 67 89</swp-data-table-cell>
<swp-data-table-cell>sofie@email.dk</swp-data-table-cell>
<swp-data-table-cell>14</swp-data-table-cell>
<swp-data-table-cell>9. dec</swp-data-table-cell>
<swp-data-table-cell>Emma L.</swp-data-table-cell>
<swp-data-table-cell>Mar 2024</swp-data-table-cell>
<swp-data-table-cell>
<swp-tag class="vip">VIP</swp-tag>
</swp-data-table-cell>
</swp-data-table-row>
</swp-data-table>
<swp-empty-state id="emptyState" style="display: none;">
<i class="ph ph-users"></i>
<span localize="customers.emptySearch">Ingen kunder matcher din søgning</span>
</swp-empty-state>
</swp-card>
@await Component.InvokeAsync("CustomerTable")
</swp-page-container>
<!-- Customer Drawer -->

View file

@ -562,5 +562,54 @@
"dailysummary": "Email med daglig oversigt"
}
}
},
"customers": {
"title": "Kunder",
"subtitle": "Administrer kunder og kundekort",
"searchPlaceholder": "Søg kunde (navn, telefon, email...)",
"export": "Eksporter",
"create": "Ny kunde",
"emptySearch": "Ingen kunder matcher din søgning",
"column": {
"name": "Navn",
"phone": "Telefon",
"email": "Email",
"visits": "Besøg",
"lastVisit": "Sidste",
"hairdresser": "Frisør",
"created": "Oprettet",
"tags": "Tags"
},
"stats": {
"total": "Total kunder",
"newThisMonth": "Nye denne måned",
"avgVisits": "Gns. besøg"
},
"drawer": {
"title": "Kundekort",
"visits": "Besøg",
"avgInterval": "Gns. interval",
"preferredHairdresser": "Foretrukken frisør",
"contactInfo": "Kontaktoplysninger",
"phone": "Telefon",
"email": "Email",
"address": "Adresse",
"zipCity": "Postnr + By",
"marketing": "Marketing",
"emailMarketing": "Email marketing",
"smsMarketing": "SMS marketing",
"profile": "Profil",
"hairType": "Hårtype",
"porosity": "Porøsitet",
"preference": "Præference",
"warnings": "Advarsler",
"revenueChart": "Omsætning (sidste 6 mdr)",
"services": "Services",
"products": "Produkter",
"recentNotes": "Seneste noter",
"noteType": "Note",
"colorFormula": "Farveformel",
"seeAllNotes": "Se alle noter →"
}
}
}

View file

@ -1,117 +1,124 @@
@model PlanTempus.Application.Features.Services.Components.ServiceDetailPricesViewModel
<swp-card>
<swp-card-header>
<swp-card-title>@Model.LabelPriceStructure</swp-card-title>
</swp-card-header>
<swp-price-mode>
<swp-price-mode-btn data-mode="simple" class="@(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Simple ? "active" : "")">@Model.LabelSimplePrice</swp-price-mode-btn>
<swp-price-mode-btn data-mode="matrix" class="@(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Matrix ? "active" : "")">@Model.LabelMatrixPrice</swp-price-mode-btn>
</swp-price-mode>
<!-- Simple price view -->
<swp-price-simple style="display: @(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Simple ? "block" : "none");">
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelPrice</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.SimplePrice</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-price-simple>
<!-- Matrix price view -->
<swp-price-matrix style="display: @(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Matrix ? "block" : "none");">
<swp-data-table class="price-matrix">
<swp-data-table-header>
<swp-data-table-cell>@Model.LabelLevel</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelShortHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelMediumHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelLongHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelExtraLongHair</swp-data-table-cell>
</swp-data-table-header>
@foreach (var row in Model.PriceMatrix)
{
<swp-data-table-row>
<swp-data-table-cell>@row.Level</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.ShortHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.MediumHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.LongHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.ExtraLongHair</span> kr</swp-data-table-cell>
</swp-data-table-row>
}
</swp-data-table>
<swp-add-button>
<i class="ph ph-plus"></i>
@Model.LabelAddLevel
</swp-add-button>
</swp-price-matrix>
</swp-card>
<swp-detail-grid>
<swp-card>
<swp-card-header>
<swp-card-title>@Model.LabelEconomy</swp-card-title>
</swp-card-header>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelVatRate</swp-edit-label>
<swp-select data-value="@Model.VatRate">
<button type="button" aria-expanded="false">
<swp-select-value>@(Model.VatRate == "25" ? "25% (standard)" : "0% (momsfri)")</swp-select-value>
<i class="ph ph-caret-down"></i>
</button>
<swp-select-dropdown>
<swp-select-option data-value="25" class="@(Model.VatRate == "25" ? "selected" : "")">25% (standard)</swp-select-option>
<swp-select-option data-value="0" class="@(Model.VatRate == "0" ? "selected" : "")">0% (momsfri)</swp-select-option>
</swp-select-dropdown>
</swp-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelProductCost</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.ProductCost</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelCommission</swp-edit-label>
<swp-select data-value="@Model.Commission">
<button type="button" aria-expanded="false">
<swp-select-value>@(Model.Commission == "standard" ? "Standard (fra lønsats)" : Model.Commission == "fixed" ? "Fast beløb" : "Procent af pris")</swp-select-value>
<i class="ph ph-caret-down"></i>
</button>
<swp-select-dropdown>
<swp-select-option data-value="standard" class="@(Model.Commission == "standard" ? "selected" : "")">Standard (fra lønsats)</swp-select-option>
<swp-select-option data-value="fixed" class="@(Model.Commission == "fixed" ? "selected" : "")">Fast beløb</swp-select-option>
<swp-select-option data-value="percent" class="@(Model.Commission == "percent" ? "selected" : "")">Procent af pris</swp-select-option>
</swp-select-dropdown>
</swp-select>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<!-- Column 1 -->
<swp-card-column>
<swp-card>
<swp-card-header>
<swp-card-title>@Model.LabelEconomy</swp-card-title>
</swp-card-header>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelVatRate</swp-edit-label>
<swp-select data-value="@Model.VatRate">
<button type="button" aria-expanded="false">
<swp-select-value>@(Model.VatRate == "25" ? "25% (standard)" : "0% (momsfri)")</swp-select-value>
<i class="ph ph-caret-down"></i>
</button>
<swp-select-dropdown>
<swp-select-option data-value="25" class="@(Model.VatRate == "25" ? "selected" : "")">25% (standard)</swp-select-option>
<swp-select-option data-value="0" class="@(Model.VatRate == "0" ? "selected" : "")">0% (momsfri)</swp-select-option>
</swp-select-dropdown>
</swp-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelProductCost</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.ProductCost</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>@Model.LabelCommission</swp-edit-label>
<swp-select data-value="@Model.Commission">
<button type="button" aria-expanded="false">
<swp-select-value>@(Model.Commission == "standard" ? "Standard (fra lønsats)" : Model.Commission == "fixed" ? "Fast beløb" : "Procent af pris")</swp-select-value>
<i class="ph ph-caret-down"></i>
</button>
<swp-select-dropdown>
<swp-select-option data-value="standard" class="@(Model.Commission == "standard" ? "selected" : "")">Standard (fra lønsats)</swp-select-option>
<swp-select-option data-value="fixed" class="@(Model.Commission == "fixed" ? "selected" : "")">Fast beløb</swp-select-option>
<swp-select-option data-value="percent" class="@(Model.Commission == "percent" ? "selected" : "")">Procent af pris</swp-select-option>
</swp-select-dropdown>
</swp-select>
</swp-edit-row>
</swp-edit-section>
</swp-card>
</swp-card-column>
<swp-card>
<!-- Column 2 -->
<swp-card-column>
<swp-card>
<swp-card-header>
<swp-card-title>@Model.LabelDiscounts</swp-card-title>
</swp-card-header>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelMemberDiscount</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.MemberDiscount ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelGiftCardPayment</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.GiftCardPayment ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelLoyaltyPoints</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.LoyaltyPoints ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card>
</swp-card-column>
<!-- Full-width: Price Structure -->
<swp-card class="full-width">
<swp-card-header>
<swp-card-title>@Model.LabelDiscounts</swp-card-title>
<swp-card-title>@Model.LabelPriceStructure</swp-card-title>
</swp-card-header>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelMemberDiscount</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.MemberDiscount ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelGiftCardPayment</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.GiftCardPayment ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<swp-toggle-label>@Model.LabelLoyaltyPoints</swp-toggle-label>
<swp-toggle-slider data-value="@(Model.LoyaltyPoints ? "yes" : "no")">
<swp-toggle-option>@Model.ToggleYes</swp-toggle-option>
<swp-toggle-option>@Model.ToggleNo</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-price-mode>
<swp-price-mode-btn data-mode="simple" class="@(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Simple ? "active" : "")">@Model.LabelSimplePrice</swp-price-mode-btn>
<swp-price-mode-btn data-mode="matrix" class="@(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Matrix ? "active" : "")">@Model.LabelMatrixPrice</swp-price-mode-btn>
</swp-price-mode>
<!-- Simple price view -->
<swp-price-simple style="display: @(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Simple ? "block" : "none");">
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>@Model.LabelPrice</swp-edit-label>
<swp-edit-value contenteditable="true">@Model.SimplePrice</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-price-simple>
<!-- Matrix price view -->
<swp-price-matrix style="display: @(Model.PriceMode == PlanTempus.Application.Features.Services.Components.PriceMode.Matrix ? "block" : "none");">
<swp-data-table class="price-matrix">
<swp-data-table-header>
<swp-data-table-cell>@Model.LabelLevel</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelShortHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelMediumHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelLongHair</swp-data-table-cell>
<swp-data-table-cell>@Model.LabelExtraLongHair</swp-data-table-cell>
</swp-data-table-header>
@foreach (var row in Model.PriceMatrix)
{
<swp-data-table-row>
<swp-data-table-cell>@row.Level</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.ShortHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.MediumHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.LongHair</span> kr</swp-data-table-cell>
<swp-data-table-cell class="mono"><span contenteditable="true">@row.ExtraLongHair</span> kr</swp-data-table-cell>
</swp-data-table-row>
}
</swp-data-table>
<swp-add-button>
<i class="ph ph-plus"></i>
@Model.LabelAddLevel
</swp-add-button>
</swp-price-matrix>
</swp-card>
</swp-detail-grid>

View file

@ -7,25 +7,75 @@
</div>
</swp-info-box>
<!-- Løn & Økonomi sektion -->
<!-- Tillægsmoduler sektion -->
<swp-modules-section>
<swp-modules-header>
<swp-modules-title>
<i class="ph ph-wallet"></i>
Løn & Økonomi
<i class="ph ph-plus-circle"></i>
Tillægsmoduler
</swp-modules-title>
</swp-modules-header>
<swp-modules-grid>
<!-- Lønberegning -->
<!-- Online Booking -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="teal">
<i class="ph ph-globe"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Online Booking</swp-module-title>
<swp-module-desc>Lad kunder booke tider online via din egen bookingside. Integreres med kalender og påmindelser.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Gavekort -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="amber">
<i class="ph ph-gift"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Gavekort</swp-module-title>
<swp-module-desc>Sælg og administrer digitale gavekort. Kunderne kan købe online eller i butikken, og indløse ved betaling.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Kasseafstemning -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-calculator"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Lønberegning</swp-module-title>
<swp-module-desc>Beregn løn, overtid, provision og ferie automatisk. Grundmodul for løneksport til eksterne systemer.</swp-module-desc>
<swp-module-title>Kasseafstemning</swp-module-title>
<swp-module-desc>Daglig kasseopgørelse og afstemning. Hold styr på kontanter, kort og andre betalingsmetoder.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
@ -42,15 +92,15 @@
</swp-module-footer>
</swp-module-card>
<!-- Intect -->
<!-- Stregkodescanner -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
<swp-module-icon class="purple">
<i class="ph ph-barcode"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Intect</swp-module-title>
<swp-module-desc>Eksporter direkte til Intect lønsystem i StandardMapping-format.</swp-module-desc>
<swp-module-title>Stregkodescanner</swp-module-title>
<swp-module-desc>Scan EAN-koder og få AI-genererede produktbeskrivelser automatisk. Opret nye produkter på sekunder.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
@ -62,92 +112,21 @@
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
<swp-module-tag class="new">AI</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Proløn -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Proløn</swp-module-title>
<swp-module-desc>Eksporter direkte til Proløn lønsystem.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
</swp-module-tags>
</swp-module-footer>
</swp-module-card>
<!-- Danløn -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Danløn</swp-module-title>
<swp-module-desc>Eksporter direkte til Danløn lønsystem.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
</swp-module-tags>
</swp-module-footer>
</swp-module-card>
<!-- Salary.dk -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Salary.dk</swp-module-title>
<swp-module-desc>Eksporter direkte til Salary.dk lønsystem.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
</swp-module-tags>
</swp-module-footer>
</swp-module-card>
<!-- Zenegy -->
<!-- Website Builder -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="teal">
<i class="ph ph-export"></i>
<i class="ph ph-layout"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Zenegy</swp-module-title>
<swp-module-desc>Eksporter direkte til Zenegy lønsystem. Automatisk overførsel af timer og provision.</swp-module-desc>
<swp-module-title>Website Builder</swp-module-title>
<swp-module-desc>Byg din salons hjemmeside med drag-and-drop blokke. Vælg mellem færdige designs, tilpas farver og fonte, og integrer din booking.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
@ -158,8 +137,53 @@
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
<swp-module-tag class="price">+149 kr/md</swp-module-tag>
<swp-module-tag class="new">Ny</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Åbn Builder</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- HR & Dokumenter - Full width -->
<swp-module-card class="full-width">
<swp-module-header>
<swp-module-icon class="purple">
<i class="ph ph-folder-user"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>HR & Dokumenter</swp-module-title>
<swp-module-desc>Komplet medarbejderstyring med alle de værktøjer du behøver for at holde styr på dit team.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-features>
<swp-module-feature class="purple">
<i class="ph ph-check-circle"></i>
<span>Kontrakter med udløbspåmindelser</span>
</swp-module-feature>
<swp-module-feature class="purple">
<i class="ph ph-check-circle"></i>
<span>Certificeringer og kursusstyring</span>
</swp-module-feature>
<swp-module-feature class="purple">
<i class="ph ph-check-circle"></i>
<span>Ferie-saldo, sygefravær og barsel</span>
</swp-module-feature>
<swp-module-feature class="purple">
<i class="ph ph-check-circle"></i>
<span>Dokumenter gemmes på dit eget Google Drive eller OneDrive</span>
</swp-module-feature>
</swp-module-features>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
</swp-modules-grid>
@ -345,170 +369,6 @@
</swp-modules-grid>
</swp-modules-section>
<!-- Tillægsmoduler sektion -->
<swp-modules-section>
<swp-modules-header>
<swp-modules-title>
<i class="ph ph-plus-circle"></i>
Tillægsmoduler
</swp-modules-title>
</swp-modules-header>
<swp-modules-grid>
<!-- Online Booking -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="teal">
<i class="ph ph-globe"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Online Booking</swp-module-title>
<swp-module-desc>Lad kunder booke tider online via din egen bookingside. Integreres med kalender og påmindelser.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Gavekort -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="amber">
<i class="ph ph-gift"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Gavekort</swp-module-title>
<swp-module-desc>Sælg og administrer digitale gavekort. Kunderne kan købe online eller i butikken, og indløse ved betaling.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Kasseafstemning -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-calculator"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Kasseafstemning</swp-module-title>
<swp-module-desc>Daglig kasseopgørelse og afstemning. Hold styr på kontanter, kort og andre betalingsmetoder.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Stregkodescanner -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="purple">
<i class="ph ph-barcode"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Stregkodescanner</swp-module-title>
<swp-module-desc>Scan EAN-koder og få AI-genererede produktbeskrivelser automatisk. Opret nye produkter på sekunder.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
<swp-module-tag class="new">AI</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Website Builder -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="teal">
<i class="ph ph-layout"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Website Builder</swp-module-title>
<swp-module-desc>Byg din salons hjemmeside med drag-and-drop blokke. Vælg mellem færdige designs, tilpas farver og fonte, og integrer din booking.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="price">+149 kr/md</swp-module-tag>
<swp-module-tag class="new">Ny</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Åbn Builder</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- HR & Dokumenter - Full width -->
<swp-module-card class="full-width">
<swp-module-header>
<swp-module-icon class="purple">
<i class="ph ph-folder-user"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>HR & Dokumenter</swp-module-title>
<swp-module-desc>Komplet medarbejderstyring: Kontrakter, certificeringer, kurser, ferie-saldo, sygefravær og barsel. Upload dokumenter og få påmindelser om udløbsdatoer.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
</swp-modules-grid>
</swp-modules-section>
<!-- Integrationer sektion -->
<swp-modules-section>
<swp-modules-header>
@ -602,3 +462,161 @@
</swp-module-card>
</swp-modules-grid>
</swp-modules-section>
<!-- Løn & Økonomi sektion -->
<swp-modules-section>
<swp-modules-header>
<swp-modules-title>
<i class="ph ph-wallet"></i>
Løn & Økonomi
</swp-modules-title>
</swp-modules-header>
<swp-modules-grid>
<!-- Lønberegning -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="teal">
<i class="ph ph-calculator"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Lønberegning</swp-module-title>
<swp-module-desc>Beregn løn, overtid, provision og ferie automatisk. Grundmodul for løneksport til eksterne systemer.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Intect -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Intect</swp-module-title>
<swp-module-desc>Eksporter direkte til Intect lønsystem i StandardMapping-format.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="included">Inkluderet</swp-module-tag>
</swp-module-tags>
<swp-btn class="secondary sm">Indstillinger</swp-btn>
</swp-module-footer>
</swp-module-card>
<!-- Proløn -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Proløn</swp-module-title>
<swp-module-desc>Eksporter direkte til Proløn lønsystem.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
</swp-module-tags>
</swp-module-footer>
</swp-module-card>
<!-- Danløn -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Danløn</swp-module-title>
<swp-module-desc>Eksporter direkte til Danløn lønsystem.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
</swp-module-tags>
</swp-module-footer>
</swp-module-card>
<!-- Salary.dk -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="blue">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Salary.dk</swp-module-title>
<swp-module-desc>Eksporter direkte til Salary.dk lønsystem.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
</swp-module-tags>
</swp-module-footer>
</swp-module-card>
<!-- Zenegy -->
<swp-module-card>
<swp-module-header>
<swp-module-icon class="teal">
<i class="ph ph-export"></i>
</swp-module-icon>
<swp-module-info>
<swp-module-title>Zenegy</swp-module-title>
<swp-module-desc>Eksporter direkte til Zenegy lønsystem. Automatisk overførsel af timer og provision.</swp-module-desc>
</swp-module-info>
<swp-module-toggle>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Til</swp-toggle-option>
<swp-toggle-option>Fra</swp-toggle-option>
</swp-toggle-slider>
</swp-module-toggle>
</swp-module-header>
<swp-module-footer>
<swp-module-tags>
<swp-module-tag class="coming">Kommer</swp-module-tag>
</swp-module-tags>
</swp-module-footer>
</swp-module-card>
</swp-modules-grid>
</swp-modules-section>