PlanTempusApp/.workbench/POC/poc-employee.html
2026-02-03 19:12:45 +01:00

4150 lines
125 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Medarbejder Detaljer - Anna Sørensen</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ==========================================
FONT FACE (Poppins)
========================================== */
@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;
}
/* ==========================================
CSS VARIABLES (Design System)
========================================== */
:root {
--color-surface: #fff;
--color-background: #f5f5f5;
--color-background-hover: #f0f0f0;
--color-background-alt: #fafafa;
--color-border: #e0e0e0;
--color-text: #333;
--color-text-secondary: #666;
--color-teal: #00897b;
--color-blue: #1976d2;
--color-red: #e53935;
--color-amber: #f59e0b;
--color-purple: #8b5cf6;
--color-green: #43a047;
--font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
/* ==========================================
RESET & BASE
========================================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
font-size: 14px;
color: var(--color-text);
background: var(--color-background);
line-height: 1.5;
}
/* ==========================================
TOPBAR
========================================== */
swp-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 100;
}
swp-topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
swp-back-link {
display: flex;
align-items: center;
gap: 6px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 13px;
cursor: pointer;
transition: color 150ms ease;
}
swp-back-link:hover {
color: var(--color-teal);
}
swp-back-link svg {
width: 16px;
height: 16px;
fill: currentColor;
}
swp-topbar-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text);
}
swp-topbar-actions {
display: flex;
align-items: center;
gap: 12px;
}
swp-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
border-radius: 6px;
border: none;
cursor: pointer;
transition: all 150ms ease;
}
swp-btn.primary {
background: var(--color-teal);
color: white;
}
swp-btn.primary:hover {
background: #00796b;
}
swp-btn.secondary {
background: var(--color-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}
swp-btn.secondary:hover {
background: var(--color-background);
}
/* ==========================================
EMPLOYEE HEADER
========================================== */
swp-employee-header {
display: flex;
gap: 24px;
padding: 24px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 49px;
z-index: 90;
}
swp-employee-avatar-large {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-teal) 0%, #00695c 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
font-weight: 600;
color: white;
flex-shrink: 0;
position: relative;
cursor: pointer;
overflow: hidden;
}
swp-employee-avatar-large img {
width: 100%;
height: 100%;
object-fit: cover;
}
swp-employee-avatar-large swp-avatar-initials {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
swp-employee-avatar-large.has-image swp-avatar-initials {
display: none;
}
swp-avatar-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
opacity: 0;
transition: opacity 150ms ease;
border-radius: 50%;
}
swp-employee-avatar-large:hover swp-avatar-overlay {
opacity: 1;
}
swp-avatar-overlay svg {
width: 24px;
height: 24px;
fill: white;
}
swp-avatar-overlay span {
font-size: 9px;
font-weight: 500;
color: white;
text-transform: uppercase;
letter-spacing: 0.3px;
}
swp-avatar-overlay .upload-text { display: block; }
swp-avatar-overlay .change-text { display: none; }
swp-employee-avatar-large.has-image swp-avatar-overlay .upload-text { display: none; }
swp-employee-avatar-large.has-image swp-avatar-overlay .change-text { display: block; }
swp-avatar-remove {
position: absolute;
top: -4px;
right: -4px;
width: 20px;
height: 20px;
background: var(--color-red);
border-radius: 50%;
display: none;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: 600;
cursor: pointer;
z-index: 2;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
transition: transform 150ms ease;
}
swp-employee-avatar-large.has-image:hover swp-avatar-remove {
display: flex;
}
swp-avatar-remove:hover {
transform: scale(1.1);
}
swp-avatar-upload-input {
display: none;
}
/* ==========================================
CHECKBOX ROW
========================================== */
swp-checkbox-list {
display: flex;
flex-direction: column;
gap: 4px;
}
swp-checkbox-row {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
transition: background 150ms ease;
}
swp-checkbox-row:hover {
background: var(--color-background-alt);
}
swp-checkbox-box {
width: 18px;
height: 18px;
border: 2px solid var(--color-border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
transition: all 150ms ease;
}
swp-checkbox-row.checked swp-checkbox-box {
background: var(--color-teal);
border-color: var(--color-teal);
}
swp-checkbox-box svg {
width: 12px;
height: 12px;
fill: white;
opacity: 0;
transition: opacity 150ms ease;
}
swp-checkbox-row.checked swp-checkbox-box svg {
opacity: 1;
}
swp-checkbox-text {
font-size: 14px;
color: var(--color-text);
line-height: 1.4;
}
/* ==========================================
SCHEDULE TABLE (read-only, single employee)
Matches poc-arbejdstidsplan.html styling
========================================== */
swp-schedule-grid {
display: grid;
grid-template-columns: 120px repeat(7, minmax(80px, 1fr));
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--color-border);
background: var(--color-surface);
}
swp-schedule-grid swp-cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12px 16px;
min-height: 60px;
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
background: var(--color-surface);
}
swp-schedule-grid swp-cell:nth-child(8n) {
border-right: none;
}
swp-schedule-grid swp-cell:nth-last-child(-n+8) {
border-bottom: none;
}
swp-schedule-grid swp-cell.header {
background: var(--color-background-alt);
font-weight: 500;
font-size: 13px;
color: var(--color-text-secondary);
min-height: 48px;
text-align: center;
}
swp-schedule-grid swp-cell.header swp-day-name {
font-weight: 500;
color: var(--color-text);
}
swp-schedule-grid swp-cell.header swp-day-date {
font-size: 12px;
color: var(--color-text-secondary);
font-weight: 400;
}
swp-schedule-grid swp-cell.header.week-col {
font-size: 13px;
font-weight: 600;
color: var(--color-text);
}
swp-schedule-grid swp-cell.week-label {
font-size: 14px;
font-weight: 500;
color: var(--color-text);
background: var(--color-background-alt);
justify-content: center;
align-items: flex-start;
padding-left: 16px;
}
swp-schedule-grid swp-cell.week-label swp-week-hours {
font-size: 12px;
font-weight: 400;
color: var(--color-text-secondary);
font-family: var(--font-mono);
margin-top: 2px;
}
swp-time-badge {
font-family: var(--font-mono);
font-size: 12px;
font-weight: 500;
padding: 4px 8px;
border-radius: 4px;
background: color-mix(in srgb, var(--color-teal) 10%, white);
color: var(--color-text);
white-space: nowrap;
min-width: 90px;
text-align: center;
display: inline-block;
}
swp-time-badge.off {
background: transparent;
color: var(--color-text-secondary);
}
swp-time-badge.vacation {
background: color-mix(in srgb, var(--color-amber) 15%, white);
color: #b45309;
}
swp-time-badge.sick {
background: color-mix(in srgb, var(--color-red) 15%, white);
color: var(--color-red);
}
swp-schedule-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
swp-schedule-header swp-section-label {
margin-bottom: 0;
}
swp-edit-link {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--color-teal);
text-decoration: none;
cursor: pointer;
transition: color 150ms ease;
}
swp-edit-link:hover {
color: #00695c;
}
swp-edit-link svg {
width: 14px;
height: 14px;
fill: currentColor;
}
swp-employee-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
swp-employee-name-row {
display: flex;
align-items: center;
gap: 16px;
}
swp-employee-name {
font-size: 24px;
font-weight: 600;
color: var(--color-text);
}
swp-tags-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
swp-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.3px;
border-radius: 4px;
background: var(--color-background);
color: var(--color-text-secondary);
}
swp-tag.master {
background: color-mix(in srgb, var(--color-purple) 15%, white);
color: var(--color-purple);
}
swp-tag.senior {
background: color-mix(in srgb, var(--color-blue) 15%, white);
color: var(--color-blue);
}
swp-tag.junior {
background: color-mix(in srgb, var(--color-amber) 15%, white);
color: #b45309;
}
swp-tag.cert {
background: color-mix(in srgb, var(--color-teal) 15%, white);
color: var(--color-teal);
}
swp-employee-status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 12px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
margin-left: auto;
}
swp-employee-status[data-active="true"] {
background: color-mix(in srgb, var(--color-green) 15%, white);
color: var(--color-green);
border: 1px solid color-mix(in srgb, var(--color-green) 30%, white);
}
swp-employee-status[data-active="false"] {
background: color-mix(in srgb, var(--color-red) 12%, white);
color: var(--color-red);
border: 1px solid color-mix(in srgb, var(--color-red) 30%, white);
}
swp-employee-status .icon {
font-size: 14px;
}
swp-fact-boxes-inline {
display: flex;
gap: 24px;
margin-top: 4px;
}
swp-fact-inline {
display: flex;
align-items: baseline;
gap: 6px;
}
swp-fact-inline-value {
font-size: 18px;
font-weight: 600;
font-family: var(--font-mono);
color: var(--color-text);
}
swp-fact-inline-label {
font-size: 12px;
color: var(--color-text-secondary);
}
.stars {
color: var(--color-amber);
}
/* ==========================================
TAB BAR
========================================== */
swp-tab-bar {
display: flex;
gap: 0;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
padding: 0 24px;
position: sticky;
top: 178px;
z-index: 80;
}
swp-tab {
padding: 14px 24px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 150ms ease;
}
swp-tab:hover {
color: var(--color-text);
background: var(--color-background-alt);
}
swp-tab.active {
color: var(--color-teal);
border-bottom-color: var(--color-teal);
}
/* ==========================================
TAB CONTENT
========================================== */
swp-tab-content {
display: none;
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
swp-tab-content.active {
display: block;
}
swp-tab-info {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
background: color-mix(in srgb, var(--color-teal) 8%, white);
border: 1px solid color-mix(in srgb, var(--color-teal) 25%, white);
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
color: var(--color-text-secondary);
line-height: 1.4;
}
swp-tab-info svg {
flex-shrink: 0;
color: var(--color-teal);
margin-top: 1px;
}
swp-tab-info strong {
color: var(--color-teal);
font-weight: 600;
}
/* ==========================================
SERVICES DRAG-DROP
========================================== */
.services-grid {
align-items: start;
}
/* Services layout with collapsible panel */
.services-layout {
display: flex;
gap: 0;
align-items: stretch;
min-height: 500px;
}
swp-services-panel {
display: flex;
transition: width 300ms ease;
overflow: hidden;
}
swp-services-panel.collapsed {
width: 40px;
}
swp-services-panel.expanded {
width: 420px;
}
swp-panel-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
min-width: 40px;
background: linear-gradient(135deg, var(--color-teal) 0%, color-mix(in srgb, var(--color-teal) 80%, #000) 100%);
color: white;
cursor: pointer;
border-radius: 8px 0 0 8px;
transition: all 200ms ease;
flex-shrink: 0;
}
swp-panel-toggle:hover {
background: linear-gradient(135deg, color-mix(in srgb, var(--color-teal) 90%, #fff) 0%, var(--color-teal) 100%);
}
swp-panel-toggle svg {
transition: transform 200ms ease;
}
swp-services-panel.expanded swp-panel-toggle svg {
transform: rotate(180deg);
}
swp-panel-content {
display: none;
flex: 1;
min-width: 0;
}
swp-services-panel.expanded swp-panel-content {
display: block;
}
swp-services-panel swp-card {
border-radius: 0 8px 8px 0;
border-left: none;
height: 100%;
}
.selected-services-card {
flex: 1;
}
/* Edit button in section label */
.selected-services-card swp-section-label {
display: flex;
align-items: center;
justify-content: space-between;
}
.edit-services-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--color-teal);
color: white;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 150ms ease;
}
.edit-services-btn:hover {
background: color-mix(in srgb, var(--color-teal) 85%, #000);
}
.edit-services-btn.active {
background: var(--color-text-secondary);
}
.services-card {
display: flex;
flex-direction: column;
}
.services-card swp-search-field {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 6px;
margin-bottom: 12px;
}
.services-card swp-search-field img {
opacity: 0.5;
flex-shrink: 0;
}
.services-card swp-search-field input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: var(--color-text);
}
.services-card swp-search-field input::placeholder {
color: var(--color-text-muted);
}
swp-services-selected,
swp-services-available {
display: flex;
flex-direction: column;
gap: 8px;
min-height: 300px;
max-height: 600px;
overflow-y: auto;
padding: 4px;
border-radius: 8px;
transition: all 150ms ease;
}
swp-services-selected.drag-over,
swp-services-available.drag-over {
background: color-mix(in srgb, var(--color-teal) 8%, white);
outline: 2px dashed var(--color-teal);
outline-offset: -2px;
}
swp-services-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 40px 20px;
color: var(--color-text-muted);
border: 2px dashed var(--color-border);
border-radius: 8px;
text-align: center;
font-size: 14px;
}
swp-services-empty svg {
opacity: 0.4;
}
/* Service category in columns */
swp-service-category-group {
display: flex;
flex-direction: column;
gap: 4px;
}
swp-service-category-header {
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 8px 0 4px 0;
border-bottom: 1px solid var(--color-border);
margin-bottom: 4px;
}
/* Service row - draggable */
swp-service-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 6px;
cursor: url('icons/drag.svg') 12 12, grab;
transition: all 150ms ease;
user-select: none;
}
swp-service-row:hover {
border-color: var(--color-teal);
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
swp-service-row:active {
cursor: url('icons/drag.svg') 12 12, grabbing;
}
swp-service-row.dragging {
opacity: 0.5;
transform: scale(0.98);
}
swp-service-row.multi-selected {
background: color-mix(in srgb, var(--color-teal) 12%, white);
border-color: var(--color-teal);
box-shadow: 0 0 0 1px var(--color-teal);
}
swp-drag-handle {
color: var(--color-text-muted);
font-size: 14px;
cursor: url('icons/drag.svg') 12 12, grab;
padding: 0 2px;
}
swp-service-row:hover swp-drag-handle {
color: var(--color-text-secondary);
}
swp-service-color {
width: 4px;
height: 28px;
border-radius: 2px;
flex-shrink: 0;
}
swp-service-info {
flex: 1;
min-width: 0;
}
swp-service-name {
font-size: 12px;
font-weight: 500;
color: var(--color-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
swp-service-meta {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
/* Price and duration columns */
swp-service-price-col {
width: 75px;
text-align: right;
font-size: 13px;
font-family: var(--font-mono);
color: var(--color-text-secondary);
flex-shrink: 0;
}
swp-service-duration-col {
width: 50px;
text-align: right;
font-size: 13px;
font-family: var(--font-mono);
color: var(--color-text-secondary);
flex-shrink: 0;
}
/* Editable inputs in selected column */
swp-services-selected swp-service-price-col,
swp-services-selected swp-service-duration-col {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
}
swp-services-selected swp-service-price-col input,
swp-services-selected swp-service-duration-col input {
width: 55px;
padding: 4px 6px;
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 12px;
font-family: var(--font-mono);
text-align: right;
background: var(--color-surface);
}
swp-services-selected swp-service-price-col input:focus,
swp-services-selected swp-service-duration-col input:focus {
border-color: var(--color-teal);
outline: none;
}
swp-services-selected swp-service-price-col input::-webkit-inner-spin-button,
swp-services-selected swp-service-price-col input::-webkit-outer-spin-button {
opacity: 1;
}
/* Override indicator */
swp-service-row.has-override swp-service-price-col,
swp-service-row.has-override swp-service-duration-col {
color: var(--color-teal);
}
/* Service values (read mode) */
swp-service-values {
display: flex;
align-items: center;
margin-left: auto;
}
swp-service-price,
swp-service-duration {
font-size: 13px;
font-family: var(--font-mono);
color: var(--color-text-secondary);
text-align: right;
}
swp-service-price {
width: 100px;
}
swp-service-duration {
width: 80px;
}
swp-service-price.modified,
swp-service-duration.modified {
color: var(--color-teal);
font-weight: 500;
}
/* Hide values in edit mode */
.edit-mode swp-service-values {
display: none;
}
/* Service controls container - hidden by default */
swp-service-controls {
display: none;
align-items: center;
margin-left: auto;
}
/* Show controls in edit mode */
.edit-mode swp-service-controls {
display: flex;
}
/* Final values with adjustable controls */
swp-service-final-values {
display: flex;
align-items: center;
gap: 12px;
}
swp-adjustable-value {
display: flex;
align-items: center;
gap: 4px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 4px;
padding: 2px;
}
swp-adjustable-value.modified {
border-color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 5%, white);
}
swp-adjustable-value .adjust-down,
swp-adjustable-value .adjust-up {
width: 24px;
height: 24px;
border: none;
background: var(--color-background);
color: var(--color-text-secondary);
font-size: 14px;
font-weight: 600;
cursor: pointer;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
transition: all 100ms ease;
}
swp-adjustable-value .adjust-down:hover,
swp-adjustable-value .adjust-up:hover {
background: var(--color-teal);
color: white;
}
swp-adjustable-value .adjust-down:active,
swp-adjustable-value .adjust-up:active {
transform: scale(0.95);
}
swp-adjustable-value .adjust-display {
min-width: 55px;
text-align: center;
font-size: 13px;
font-family: var(--font-mono);
color: var(--color-text);
padding: 0 4px;
}
swp-adjustable-value.modified .adjust-display {
color: var(--color-teal);
font-weight: 500;
}
/* Duration display */
.duration-display {
min-width: 50px;
text-align: right;
font-size: 13px;
font-family: var(--font-mono);
color: var(--color-text-secondary);
}
.duration-display.modified {
color: var(--color-teal);
font-weight: 500;
}
/* Original values (read mode) */
swp-service-originals {
display: flex;
align-items: center;
gap: 16px;
margin-right: 16px;
}
swp-service-originals .original-price-val,
swp-service-originals .original-duration-val {
font-family: var(--font-mono);
font-size: 15px;
color: var(--color-text-secondary);
}
/* Strikethrough only when value is changed */
swp-service-originals .original-price-val.struck,
swp-service-originals .original-duration-val.struck {
text-decoration: line-through;
text-decoration-color: var(--color-teal);
text-decoration-thickness: 1px;
color: var(--color-text-muted);
}
/* Original values (edit mode) - hidden by default, shown when has-override */
swp-service-originals-edit {
display: none;
align-items: center;
gap: 16px;
margin-right: 20px;
}
swp-service-row.has-override swp-service-originals-edit {
display: flex;
}
swp-service-originals-edit .original-price-val,
swp-service-originals-edit .original-duration-val {
font-family: var(--font-mono);
font-size: 15px;
color: var(--color-text-secondary);
}
swp-service-originals-edit .original-price-val.struck,
swp-service-originals-edit .original-duration-val.struck {
text-decoration: line-through;
text-decoration-color: var(--color-teal);
text-decoration-thickness: 1px;
color: var(--color-text-muted);
}
/* Highlight originals when value changes */
swp-service-originals.highlight .original-price-val,
swp-service-originals.highlight .original-duration-val,
swp-service-originals-edit.highlight .original-price-val,
swp-service-originals-edit.highlight .original-duration-val {
animation: flash-text 300ms ease;
}
@keyframes pulse-border {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-teal) 40%, transparent); }
100% { box-shadow: 0 0 0 4px transparent; }
}
@keyframes flash-text {
0% { opacity: 0.5; }
100% { opacity: 1; }
}
/* ==========================================
LAYOUT GRID
========================================== */
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
@media (max-width: 900px) {
.grid-2 { grid-template-columns: 1fr; }
.grid-4 { grid-template-columns: repeat(2, 1fr); }
}
/* ==========================================
CARDS
========================================== */
swp-card {
display: block;
background: var(--color-surface);
border-radius: 8px;
padding: 20px;
border: 1px solid var(--color-border);
margin-bottom: 16px;
}
swp-card:last-child {
margin-bottom: 0;
}
swp-section-label {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 16px;
}
/* ==========================================
STAT CARDS
========================================== */
swp-stat-card {
background: var(--color-surface);
border-radius: 8px;
padding: 20px;
text-align: center;
border: 1px solid var(--color-border);
}
swp-stat-value {
display: block;
font-size: 28px;
font-weight: 600;
font-family: var(--font-mono);
color: var(--color-text);
}
swp-stat-label {
display: block;
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 4px;
}
swp-stat-subtitle {
display: block;
font-size: 11px;
color: var(--color-text-muted);
margin-top: 2px;
}
swp-stat-card.highlight swp-stat-value {
color: var(--color-teal);
}
/* ==========================================
EDIT SECTION
========================================== */
swp-edit-section {
display: flex;
flex-direction: column;
gap: 12px;
}
swp-edit-row {
display: grid;
grid-template-columns: 140px 1fr;
align-items: center;
gap: 12px;
}
swp-edit-label {
font-size: 13px;
color: var(--color-text-secondary);
}
swp-edit-value {
font-size: 14px;
padding: 8px 12px;
border-radius: 4px;
background: var(--color-background-alt);
border: 1px solid transparent;
transition: all 150ms ease;
cursor: text;
}
swp-edit-value:hover {
background: var(--color-background);
}
swp-edit-value:focus {
outline: none;
background: var(--color-surface);
border-color: var(--color-teal);
}
swp-edit-value.masked {
letter-spacing: 1px;
}
swp-edit-select select {
width: 100%;
font-size: 14px;
font-family: inherit;
padding: 8px 12px;
border-radius: 4px;
background: var(--color-background-alt);
border: 1px solid transparent;
cursor: pointer;
transition: all 150ms ease;
}
swp-edit-select select:hover {
background: var(--color-background);
}
swp-edit-select select:focus {
outline: none;
background: var(--color-surface);
border-color: var(--color-teal);
}
/* ==========================================
TOGGLE SLIDER
========================================== */
swp-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
}
swp-toggle-row:last-child {
border-bottom: none;
}
swp-toggle-label {
font-size: 14px;
color: var(--color-text);
}
swp-toggle-description {
display: block;
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
swp-toggle-slider {
display: inline-flex;
width: fit-content;
background: var(--color-background);
border-radius: 6px;
border: 1px solid var(--color-border);
overflow: hidden;
position: relative;
}
swp-toggle-slider::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: calc(50% - 4px);
height: calc(100% - 4px);
background: color-mix(in srgb, var(--color-green) 18%, white);
border-radius: 4px;
transition: transform 200ms ease, background 200ms ease;
}
swp-toggle-slider[data-value="no"]::before {
transform: translateX(100%);
background: color-mix(in srgb, var(--color-red) 18%, white);
}
swp-toggle-option {
position: relative;
z-index: 1;
padding: 5px 16px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: color 150ms ease;
user-select: none;
}
swp-toggle-slider[data-value="yes"] swp-toggle-option:first-child {
color: var(--color-green);
font-weight: 600;
}
swp-toggle-slider[data-value="no"] swp-toggle-option:last-child {
color: var(--color-red);
font-weight: 600;
}
/* ==========================================
AVAILABILITY LIST
========================================== */
swp-availability-list {
display: flex;
flex-direction: column;
}
swp-availability-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 16px;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
swp-availability-row:last-child {
border-bottom: none;
}
swp-availability-row[data-enabled="false"] {
opacity: 0.5;
}
swp-availability-day {
width: 80px;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
swp-availability-time {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
swp-time-range {
display: flex;
align-items: center;
gap: 12px;
}
swp-time-range-slider {
position: relative;
flex: 1;
height: 20px;
display: flex;
align-items: center;
}
swp-time-range-track {
position: absolute;
width: 100%;
height: 4px;
background: var(--color-border);
border-radius: 2px;
}
swp-time-range-fill {
position: absolute;
height: 4px;
background: var(--color-teal);
border-radius: 2px;
cursor: grab;
}
swp-time-range-slider input[type="range"] {
position: absolute;
width: 100%;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: transparent;
pointer-events: none;
margin: 0;
}
swp-time-range-slider input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: var(--color-teal);
border: 2px solid white;
border-radius: 50%;
cursor: pointer;
pointer-events: auto;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
swp-time-range-label {
font-size: 12px;
font-family: var(--font-mono);
color: var(--color-text);
min-width: 90px;
text-align: center;
background: var(--color-background-alt);
padding: 3px 8px;
border-radius: 4px;
}
/* ==========================================
VACATION LIST
========================================== */
swp-vacation-list {
display: flex;
flex-direction: column;
gap: 8px;
}
swp-vacation-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-background-alt);
border-radius: 6px;
border: 1px solid var(--color-border);
}
swp-vacation-dates {
flex: 1;
font-size: 14px;
font-family: var(--font-mono);
}
swp-vacation-type {
font-size: 12px;
padding: 3px 10px;
border-radius: 12px;
font-weight: 500;
}
swp-vacation-type.ferie {
background: color-mix(in srgb, var(--color-amber) 15%, white);
color: #b45309;
}
swp-vacation-type.fri {
background: color-mix(in srgb, var(--color-purple) 15%, white);
color: var(--color-purple);
}
swp-vacation-type.syg {
background: color-mix(in srgb, var(--color-red) 15%, white);
color: var(--color-red);
}
swp-vacation-delete {
padding: 4px 8px;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: 4px;
transition: all 150ms ease;
}
swp-vacation-delete:hover {
color: var(--color-red);
background: color-mix(in srgb, var(--color-red) 10%, white);
}
/* ==========================================
SERVICE LIST
========================================== */
swp-service-list {
display: flex;
flex-direction: column;
gap: 8px;
}
swp-service-item {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: var(--color-background-alt);
border-radius: 6px;
border: 1px solid var(--color-border);
cursor: pointer;
transition: border-color 0.15s ease;
}
swp-service-item:hover {
border-color: var(--color-teal);
}
swp-service-item.selected {
border-color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 5%, var(--color-background-alt));
}
swp-service-checkbox {
width: 20px;
height: 20px;
border: 2px solid var(--color-border);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.15s ease;
}
swp-service-item.selected swp-service-checkbox {
background: var(--color-teal);
border-color: var(--color-teal);
color: white;
}
swp-service-checkbox svg {
width: 14px;
height: 14px;
fill: currentColor;
opacity: 0;
}
swp-service-item.selected swp-service-checkbox svg {
opacity: 1;
}
swp-service-color {
width: 4px;
height: 40px;
border-radius: 2px;
flex-shrink: 0;
}
swp-service-info {
flex: 1;
}
swp-service-name {
font-weight: 500;
color: var(--color-text);
}
swp-service-meta {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
swp-service-override {
font-size: 13px;
color: var(--color-text-secondary);
}
swp-service-override-value {
color: var(--color-text);
font-weight: 500;
}
/* ==========================================
CERTIFICATION LIST
========================================== */
swp-cert-list {
display: flex;
flex-direction: column;
gap: 8px;
}
swp-cert-item {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: var(--color-background-alt);
border-radius: 6px;
border: 1px solid var(--color-border);
}
swp-cert-icon {
width: 36px;
height: 36px;
background: color-mix(in srgb, var(--color-teal) 15%, white);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
swp-cert-info {
flex: 1;
}
swp-cert-name {
font-weight: 500;
color: var(--color-text);
}
swp-cert-dates {
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
swp-cert-status {
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
border-radius: 12px;
}
swp-cert-status.valid {
background: color-mix(in srgb, var(--color-green) 15%, white);
color: var(--color-green);
}
swp-cert-status.expiring {
background: color-mix(in srgb, var(--color-amber) 15%, white);
color: #b45309;
}
swp-cert-status.expired {
background: color-mix(in srgb, var(--color-red) 15%, white);
color: var(--color-red);
}
/* ==========================================
SALARY TABLE
========================================== */
swp-salary-history {
display: block;
margin-top: 16px;
}
swp-salary-history table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
swp-salary-history th,
swp-salary-history td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
swp-salary-history th {
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
background: var(--color-background-alt);
}
swp-salary-history td {
color: var(--color-text);
}
swp-salary-history td.mono {
font-family: var(--font-mono);
}
/* ==========================================
BOOKING TABLE
========================================== */
swp-booking-table {
display: block;
margin-top: 16px;
}
swp-booking-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
swp-booking-table th,
swp-booking-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
swp-booking-table th {
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
background: var(--color-background-alt);
}
swp-booking-table td {
color: var(--color-text);
}
swp-booking-table .customer {
font-weight: 500;
}
swp-booking-table .date {
font-family: var(--font-mono);
font-size: 12px;
color: var(--color-text-secondary);
}
swp-booking-table .amount {
font-family: var(--font-mono);
font-weight: 500;
}
/* ==========================================
INVOICE TABLE (afsluttede bookinger)
========================================== */
swp-invoice-table {
display: block;
margin-top: 16px;
}
swp-invoice-table table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
swp-invoice-table th,
swp-invoice-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--color-border);
}
swp-invoice-table th {
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.3px;
background: var(--color-background-alt);
}
swp-invoice-table td {
color: var(--color-text);
}
swp-invoice-table .date,
swp-invoice-table .time {
font-family: var(--font-mono);
font-size: 12px;
color: var(--color-text-secondary);
}
swp-invoice-table .customer {
font-weight: 500;
}
swp-invoice-table .services {
max-width: 250px;
}
swp-invoice-table .duration {
font-family: var(--font-mono);
font-size: 12px;
color: var(--color-text-secondary);
}
swp-invoice-table .amount {
font-family: var(--font-mono);
font-weight: 600;
text-align: right;
}
swp-invoice-table .amount-col {
text-align: right;
}
/* Status badges */
swp-status-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
}
swp-status-badge.paid {
background: color-mix(in srgb, var(--color-teal) 15%, white);
color: var(--color-teal);
}
swp-status-badge.pending {
background: color-mix(in srgb, #f59e0b 15%, white);
color: #d97706;
}
/* ==========================================
CHART SECTION (matches poc-detail-drawer.html)
========================================== */
swp-chart-section {
display: block;
}
swp-chart-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
swp-chart-title {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
}
swp-chart-subtitle {
display: block;
font-size: 12px;
color: var(--color-text-muted);
margin-top: 2px;
}
swp-chart-legend {
display: flex;
gap: 16px;
}
swp-chart-legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--color-text-secondary);
}
swp-chart-legend-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
swp-chart-legend-dot.services {
background: var(--color-teal);
}
swp-chart-legend-dot.products {
background: var(--color-blue);
}
swp-chart-container {
display: block;
background: var(--color-background-alt);
border-radius: 8px;
border: 1px solid var(--color-border);
padding: 16px;
}
/* ==========================================
ADD BUTTON
========================================== */
swp-add-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
border: 2px dashed var(--color-border);
border-radius: 6px;
color: var(--color-text-secondary);
font-size: 13px;
cursor: pointer;
transition: all 150ms ease;
margin-top: 12px;
}
swp-add-button:hover {
border-color: var(--color-teal);
color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 5%, white);
}
/* Duration slider (same style as time-range-slider) */
swp-duration-slider {
position: relative;
width: 80px;
height: 20px;
display: flex;
align-items: center;
}
swp-duration-slider swp-slider-track {
position: absolute;
width: 100%;
height: 4px;
background: var(--color-border);
border-radius: 2px;
}
swp-duration-slider input[type="range"] {
position: absolute;
width: 100%;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: transparent;
margin: 0;
}
swp-duration-slider input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: var(--color-teal);
border: 2px solid white;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
swp-duration-slider input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
background: var(--color-teal);
border: 2px solid white;
border-radius: 50%;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
/* Compact service list (main view) */
swp-service-compact-list {
display: flex;
flex-direction: column;
gap: 6px;
}
swp-service-compact-category {
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 12px;
padding-bottom: 4px;
}
swp-service-compact-category:first-child {
margin-top: 0;
}
swp-service-compact-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--color-background-alt);
border-radius: 6px;
}
swp-service-compact-color {
width: 4px;
height: 24px;
border-radius: 2px;
flex-shrink: 0;
}
swp-service-compact-name {
flex: 1;
font-size: 14px;
color: var(--color-text);
}
swp-service-compact-override {
display: flex;
gap: 8px;
font-size: 12px;
font-family: var(--font-mono);
}
swp-service-compact-override .duration-override {
color: var(--color-teal);
padding: 2px 8px;
background: color-mix(in srgb, var(--color-teal) 10%, white);
border-radius: 4px;
}
swp-service-compact-override .price-override {
color: var(--color-teal);
padding: 2px 8px;
background: color-mix(in srgb, var(--color-teal) 10%, white);
border-radius: 4px;
}
swp-service-compact-override .price-override s {
color: var(--color-text-muted);
margin-right: 4px;
}
swp-service-compact-override.standard {
display: none;
}
swp-service-compact-empty {
display: block;
padding: 24px 16px;
text-align: center;
color: var(--color-text-muted);
font-size: 14px;
}
/* Edit link for services card */
swp-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
swp-card-header swp-section-label {
margin-bottom: 0;
}
swp-edit-link {
font-size: 13px;
color: var(--color-teal);
cursor: pointer;
display: flex;
align-items: center;
gap: 4px;
transition: color 150ms ease;
}
swp-edit-link:hover {
color: #00695c;
}
swp-edit-link svg {
width: 14px;
height: 14px;
fill: currentColor;
}
/* ==========================================
HR TAB - DOCUMENTS
========================================== */
swp-document-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 16px;
}
swp-document-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--color-background-alt);
border-radius: 6px;
border: 1px solid var(--color-border);
}
swp-document-icon {
font-size: 20px;
}
swp-document-info {
flex: 1;
}
swp-document-name {
display: block;
font-weight: 500;
color: var(--color-text);
}
swp-document-meta {
display: block;
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
swp-document-actions {
display: flex;
gap: 8px;
}
/* ==========================================
HR TAB - COURSES
========================================== */
swp-course-section {
margin-bottom: 20px;
}
swp-course-section:last-of-type {
margin-bottom: 0;
}
swp-course-section-title {
display: block;
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 8px;
}
swp-course-list {
display: flex;
flex-direction: column;
gap: 6px;
}
swp-course-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: var(--color-background-alt);
border-radius: 6px;
}
swp-course-item.upcoming {
background: color-mix(in srgb, var(--color-teal) 8%, white);
border: 1px solid color-mix(in srgb, var(--color-teal) 20%, white);
}
swp-course-info {
flex: 1;
}
swp-course-name {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
swp-course-meta {
display: block;
font-size: 12px;
color: var(--color-text-secondary);
margin-top: 2px;
}
swp-course-status {
font-size: 11px;
font-weight: 500;
padding: 4px 10px;
border-radius: 12px;
background: color-mix(in srgb, var(--color-teal) 15%, white);
color: var(--color-teal);
}
/* ==========================================
RATES DRAWER
========================================== */
swp-drawer-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.3);
z-index: 999;
}
swp-drawer-overlay.open {
display: block;
}
swp-rates-drawer {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 420px;
background: var(--color-surface);
box-shadow: -4px 0 20px rgba(0,0,0,0.15);
z-index: 1000;
flex-direction: column;
}
swp-rates-drawer.open {
display: flex;
}
swp-drawer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
}
swp-drawer-title {
font-size: 16px;
font-weight: 600;
}
swp-drawer-close {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 150ms ease;
}
swp-drawer-close:hover {
background: var(--color-background);
color: var(--color-text);
}
swp-drawer-body {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
swp-rate-row {
display: grid;
grid-template-columns: 28px 1fr 100px;
align-items: center;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
swp-rate-row:last-child {
border-bottom: none;
}
swp-rate-row input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--color-teal);
}
swp-rate-label {
font-size: 14px;
}
swp-rate-label.disabled {
opacity: 0.4;
}
swp-rate-input {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: var(--color-text-secondary);
}
swp-rate-input input {
width: 70px;
padding: 6px 8px;
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 13px;
font-family: var(--font-mono);
text-align: right;
}
swp-rate-input.disabled input {
opacity: 0.4;
background: var(--color-background);
}
.btn-link {
background: none;
border: none;
color: var(--color-teal);
font-size: 13px;
cursor: pointer;
padding: 0;
}
.btn-link:hover {
text-decoration: underline;
}
swp-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
swp-card-header swp-section-label {
margin-bottom: 0;
}
.mono {
font-family: var(--font-mono);
}
</style>
</head>
<body>
<!-- Topbar -->
<swp-topbar>
<swp-topbar-left>
<swp-back-link>
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
Tilbage til medarbejdere
</swp-back-link>
<swp-topbar-title>Medarbejder detaljer</swp-topbar-title>
</swp-topbar-left>
<swp-topbar-actions>
<swp-btn class="secondary">Slet medarbejder</swp-btn>
<swp-btn class="primary">Gem ændringer</swp-btn>
</swp-topbar-actions>
</swp-topbar>
<!-- Employee Header -->
<swp-employee-header>
<swp-employee-avatar-large id="avatarUpload">
<swp-avatar-initials>AS</swp-avatar-initials>
<swp-avatar-overlay>
<svg viewBox="0 0 24 24"><path d="M12 15.2c1.77 0 3.2-1.43 3.2-3.2 0-1.77-1.43-3.2-3.2-3.2-1.77 0-3.2 1.43-3.2 3.2 0 1.77 1.43 3.2 3.2 3.2zm8-10.2h-3.17L15 3H9L7.17 5H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-8 13c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"/></svg>
<span class="upload-text">Upload</span>
<span class="change-text">Skift</span>
</swp-avatar-overlay>
<swp-avatar-remove id="avatarRemove" title="Fjern billede"></swp-avatar-remove>
<input type="file" accept="image/*" id="avatarInput" style="display: none;">
</swp-employee-avatar-large>
<swp-employee-info>
<swp-employee-name-row>
<swp-employee-name contenteditable="true">Anna Sørensen</swp-employee-name>
<swp-tags-row>
<swp-tag class="master">Master Stylist</swp-tag>
<swp-tag class="cert">Farvecertificeret</swp-tag>
<swp-tag class="cert">Balayage</swp-tag>
</swp-tags-row>
<swp-employee-status data-active="true">
<span class="icon"></span>
<span class="text">Aktiv</span>
</swp-employee-status>
</swp-employee-name-row>
<swp-fact-boxes-inline>
<swp-fact-inline>
<swp-fact-inline-value>248</swp-fact-inline-value>
<swp-fact-inline-label>bookinger i år</swp-fact-inline-label>
</swp-fact-inline>
<swp-fact-inline>
<swp-fact-inline-value>186.450 kr</swp-fact-inline-value>
<swp-fact-inline-label>omsætning i år</swp-fact-inline-label>
</swp-fact-inline>
<swp-fact-inline>
<swp-fact-inline-value class="stars">★★★★★</swp-fact-inline-value>
<swp-fact-inline-label>4.9 rating</swp-fact-inline-label>
</swp-fact-inline>
<swp-fact-inline>
<swp-fact-inline-value>2019</swp-fact-inline-value>
<swp-fact-inline-label>ansat siden</swp-fact-inline-label>
</swp-fact-inline>
</swp-fact-boxes-inline>
</swp-employee-info>
</swp-employee-header>
<!-- Tab Bar -->
<swp-tab-bar>
<swp-tab class="active" data-tab="general">Generelt</swp-tab>
<swp-tab data-tab="hours">Arbejdstid</swp-tab>
<swp-tab data-tab="services">Services</swp-tab>
<swp-tab data-tab="salary">Løn</swp-tab>
<swp-tab data-tab="hr">HR</swp-tab>
<swp-tab data-tab="stats">Statistik</swp-tab>
</swp-tab-bar>
<!-- ==========================================
GENERELT TAB
========================================== -->
<swp-tab-content class="active" data-tab="general">
<div class="grid-2">
<div>
<swp-card>
<swp-section-label>Kontaktoplysninger</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Fulde navn</swp-edit-label>
<swp-edit-value contenteditable="true">Anna Sørensen</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>E-mail</swp-edit-label>
<swp-edit-value contenteditable="true">anna@salon.dk</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Telefon</swp-edit-label>
<swp-edit-value contenteditable="true">+45 12 34 56 78</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Adresse</swp-edit-label>
<swp-edit-value contenteditable="true">Vestergade 15, 3. tv</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Postnr. & By</swp-edit-label>
<swp-edit-value contenteditable="true">8000 Aarhus C</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<swp-card>
<swp-section-label>Personlige oplysninger</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Fødselsdato</swp-edit-label>
<swp-edit-value contenteditable="true">15. marts 1992</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>CPR-nummer</swp-edit-label>
<swp-edit-value class="masked" contenteditable="true">150392-****</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Nødkontakt</swp-edit-label>
<swp-edit-value contenteditable="true">Peter Sørensen (ægtefælle)</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Nødkontakt tlf.</swp-edit-label>
<swp-edit-value contenteditable="true">+45 87 65 43 21</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
</div>
<div>
<swp-card>
<swp-section-label>Ansættelse</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Ansættelsesdato</swp-edit-label>
<swp-edit-value contenteditable="true">1. august 2019</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Stilling</swp-edit-label>
<swp-edit-select>
<select>
<option>Junior Stylist</option>
<option>Stylist</option>
<option>Senior Stylist</option>
<option selected>Master Stylist</option>
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Ansættelsestype</swp-edit-label>
<swp-edit-select>
<select>
<option selected>Fuldtid</option>
<option>Deltid</option>
<option>Vikar</option>
<option>Elev</option>
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Timer/uge</swp-edit-label>
<swp-edit-value contenteditable="true">37</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<swp-card>
<swp-section-label>Indstillinger</swp-section-label>
<swp-toggle-row>
<div>
<swp-toggle-label>Vis i online booking</swp-toggle-label>
<swp-toggle-description>Kunder kan vælge denne medarbejder</swp-toggle-description>
</div>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<div>
<swp-toggle-label>Modtag SMS-påmindelser</swp-toggle-label>
<swp-toggle-description>Få besked om nye bookinger</swp-toggle-description>
</div>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
<swp-toggle-row>
<div>
<swp-toggle-label>Kan redigere egen kalender</swp-toggle-label>
<swp-toggle-description>Tillad ændringer i egne bookinger</swp-toggle-description>
</div>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-card>
<swp-card>
<swp-section-label>Notifikationer</swp-section-label>
<p style="font-size: 13px; color: var(--color-text-secondary); margin-bottom: 16px;">
Vælg hvilke email-notifikationer medarbejderen skal modtage.
</p>
<swp-checkbox-list>
<swp-checkbox-row class="checked">
<swp-checkbox-box>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</swp-checkbox-box>
<swp-checkbox-text>Modtag email ved online booking</swp-checkbox-text>
</swp-checkbox-row>
<swp-checkbox-row class="checked">
<swp-checkbox-box>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</swp-checkbox-box>
<swp-checkbox-text>Modtag email ved manuel booking</swp-checkbox-text>
</swp-checkbox-row>
<swp-checkbox-row>
<swp-checkbox-box>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</swp-checkbox-box>
<swp-checkbox-text>Modtag email ved aflysning</swp-checkbox-text>
</swp-checkbox-row>
<swp-checkbox-row>
<swp-checkbox-box>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</swp-checkbox-box>
<swp-checkbox-text>Modtag email ved opskrivning til venteliste</swp-checkbox-text>
</swp-checkbox-row>
<swp-checkbox-row class="checked">
<swp-checkbox-box>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</swp-checkbox-box>
<swp-checkbox-text>Modtag daglig oversigt over morgendagens bookinger</swp-checkbox-text>
</swp-checkbox-row>
</swp-checkbox-list>
</swp-card>
</div>
</div>
</swp-tab-content>
<!-- ==========================================
ARBEJDSTID TAB
========================================== -->
<swp-tab-content data-tab="hours">
<swp-card>
<swp-schedule-header>
<swp-section-label>Arbejdstidsplan</swp-section-label>
<swp-edit-link href="poc-arbejdstidsplan.html">
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
Rediger i arbejdstidsplan
</swp-edit-link>
</swp-schedule-header>
<swp-schedule-grid>
<!-- Header row -->
<swp-cell class="header week-col"></swp-cell>
<swp-cell class="header"><swp-day-name>Mandag</swp-day-name></swp-cell>
<swp-cell class="header"><swp-day-name>Tirsdag</swp-day-name></swp-cell>
<swp-cell class="header"><swp-day-name>Onsdag</swp-day-name></swp-cell>
<swp-cell class="header"><swp-day-name>Torsdag</swp-day-name></swp-cell>
<swp-cell class="header"><swp-day-name>Fredag</swp-day-name></swp-cell>
<swp-cell class="header"><swp-day-name>Lørdag</swp-day-name></swp-cell>
<swp-cell class="header"><swp-day-name>Søndag</swp-day-name></swp-cell>
<!-- Uge 51 -->
<swp-cell class="week-label">Uge 51<swp-week-hours>38 timer</swp-week-hours></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>10:00 - 18:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 15:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<!-- Uge 52 -->
<swp-cell class="week-label">Uge 52<swp-week-hours>20 timer</swp-week-hours></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 13:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="vacation">Ferie</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="vacation">Ferie</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="vacation">Ferie</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="vacation">Ferie</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="vacation">Ferie</swp-time-badge></swp-cell>
<!-- Uge 1 -->
<swp-cell class="week-label">Uge 1<swp-week-hours>0 timer</swp-week-hours></swp-cell>
<swp-cell><swp-time-badge class="vacation">Ferie</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="vacation">Ferie</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<!-- Uge 2 -->
<swp-cell class="week-label">Uge 2<swp-week-hours>38 timer</swp-week-hours></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>10:00 - 18:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 15:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<!-- Uge 3 -->
<swp-cell class="week-label">Uge 3<swp-week-hours>38 timer</swp-week-hours></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>10:00 - 18:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 15:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<!-- Uge 4 -->
<swp-cell class="week-label">Uge 4<swp-week-hours>38 timer</swp-week-hours></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>10:00 - 18:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 17:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge>09:00 - 15:00</swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
<swp-cell><swp-time-badge class="off"></swp-time-badge></swp-cell>
</swp-schedule-grid>
</swp-card>
</swp-tab-content>
<!-- ==========================================
SERVICES TAB
========================================== -->
<swp-tab-content data-tab="services">
<swp-tab-info>
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>
<span>Priserne vises for stillingen "<strong>Master Stylist</strong>". <strong>Tip:</strong> Hold Shift nede for at vælge en række, eller Ctrl for at vælge flere enkeltvis.</span>
</swp-tab-info>
<div class="services-layout">
<!-- Venstre panel: Tilgængelige services (collapsible) -->
<swp-services-panel id="servicesPanel" class="collapsed">
<swp-panel-toggle id="panelToggle" title="Klik for at redigere services">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
</swp-panel-toggle>
<swp-panel-content>
<swp-card class="services-card">
<swp-section-label>Tilgængelige services</swp-section-label>
<swp-search-field>
<img src="icons/search.svg" alt="Søg" width="16" height="16">
<input type="text" id="serviceSearch" placeholder="Søg i services...">
</swp-search-field>
<swp-services-available id="availableServices">
<!-- Services vil blive indsat her -->
</swp-services-available>
</swp-card>
</swp-panel-content>
</swp-services-panel>
<!-- Højre kolonne: Valgte services -->
<swp-card class="services-card selected-services-card">
<swp-section-label>
Valgte services
<button type="button" id="editServicesBtn" class="edit-services-btn">
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
Rediger
</button>
</swp-section-label>
<swp-services-selected id="selectedServices">
<swp-services-empty>
<svg viewBox="0 0 24 24" width="32" height="32"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Træk services hertil</span>
</swp-services-empty>
</swp-services-selected>
</swp-card>
</div>
</swp-tab-content>
<!-- ==========================================
LØN TAB
========================================== -->
<swp-tab-content data-tab="salary">
<div class="grid-2">
<div>
<swp-card>
<swp-card-header>
<swp-section-label>Satser</swp-section-label>
<button class="btn-link" onclick="openRatesDrawer()">Rediger</button>
</swp-card-header>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Normal (timeløn)</swp-edit-label>
<swp-edit-value class="mono">131,49 kr</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Overarbejde (100%)</swp-edit-label>
<swp-edit-value class="mono">280,50 kr</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Ferie m. løn</swp-edit-label>
<swp-edit-value class="mono">140,25 kr</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<swp-card>
<swp-section-label>Provision</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Minimum pr. time</swp-edit-label>
<swp-edit-value class="mono" contenteditable="true">220 kr</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>På services</swp-edit-label>
<swp-edit-value class="mono" contenteditable="true">15%</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>På produktsalg</swp-edit-label>
<swp-edit-value class="mono" contenteditable="true">15%</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<swp-card>
<swp-section-label>Tillæg</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>8-21 Hverdage</swp-edit-label>
<swp-edit-value class="mono">28,03 kr</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>8-21 Lørdage</swp-edit-label>
<swp-edit-value class="mono">56,02 kr</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Søndag</swp-edit-label>
<swp-edit-value class="mono">112,07 kr</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
</div>
<swp-card>
<swp-section-label>Lønspecifikationer</swp-section-label>
<swp-salary-history>
<table>
<thead>
<tr>
<th>Periode</th>
<th>Bruttoløn</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>Januar 2026</td>
<td class="mono">34.063,50 kr</td>
<td><a href="poc-loenspecifikation.html" class="link">Vis</a></td>
</tr>
<tr>
<td>December 2025</td>
<td class="mono">31.845,00 kr</td>
<td><a href="poc-loenspecifikation.html" class="link">Vis</a></td>
</tr>
<tr>
<td>November 2025</td>
<td class="mono">33.290,25 kr</td>
<td><a href="poc-loenspecifikation.html" class="link">Vis</a></td>
</tr>
<tr>
<td>Oktober 2025</td>
<td class="mono">32.156,75 kr</td>
<td><a href="poc-loenspecifikation.html" class="link">Vis</a></td>
</tr>
<tr>
<td>September 2025</td>
<td class="mono">34.520,00 kr</td>
<td><a href="poc-loenspecifikation.html" class="link">Vis</a></td>
</tr>
</tbody>
</table>
</swp-salary-history>
</swp-card>
</div>
</swp-tab-content>
<!-- ==========================================
HR TAB
========================================== -->
<swp-tab-content data-tab="hr">
<div class="grid-2">
<!-- Venstre kolonne -->
<div>
<!-- Kontrakt & Dokumenter -->
<swp-card>
<swp-section-label>Kontrakt & Dokumenter</swp-section-label>
<!-- Kontrakt info -->
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Kontrakttype</swp-edit-label>
<swp-edit-select>
<select>
<option selected>Fastansættelse</option>
<option>Tidsbegrænset</option>
<option>Freelance</option>
<option>Elev/Lærling</option>
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Opsigelsesvarsel</swp-edit-label>
<swp-edit-select>
<select>
<option>14 dage</option>
<option selected>1 måned</option>
<option>3 måneder</option>
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Kontraktudløb</swp-edit-label>
<swp-edit-value>— (ingen udløb)</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
<!-- Dokument liste -->
<swp-document-list>
<swp-document-item>
<swp-document-icon>📄</swp-document-icon>
<swp-document-info>
<swp-document-name>Ansættelseskontrakt.pdf</swp-document-name>
<swp-document-meta>Uploadet 1. aug 2019</swp-document-meta>
</swp-document-info>
<swp-document-actions>
<swp-btn class="small secondary">Vis</swp-btn>
</swp-document-actions>
</swp-document-item>
<swp-document-item>
<swp-document-icon>📄</swp-document-icon>
<swp-document-info>
<swp-document-name>Tillæg - Lønforhøjelse 2023.pdf</swp-document-name>
<swp-document-meta>Uploadet 15. jan 2023</swp-document-meta>
</swp-document-info>
<swp-document-actions>
<swp-btn class="small secondary">Vis</swp-btn>
</swp-document-actions>
</swp-document-item>
</swp-document-list>
<swp-add-button>
+ Upload dokument
</swp-add-button>
</swp-card>
</div>
<!-- Højre kolonne -->
<div>
<!-- Certificeringer -->
<swp-card>
<swp-section-label>Certificeringer</swp-section-label>
<swp-cert-list>
<swp-cert-item>
<swp-cert-icon>🎓</swp-cert-icon>
<swp-cert-info>
<swp-cert-name>Balayage Specialist</swp-cert-name>
<swp-cert-dates>Udløber: 15. juni 2026</swp-cert-dates>
</swp-cert-info>
<swp-cert-status class="valid">Gyldig</swp-cert-status>
</swp-cert-item>
<swp-cert-item>
<swp-cert-icon>🎓</swp-cert-icon>
<swp-cert-info>
<swp-cert-name>Farvecertificering (Wella)</swp-cert-name>
<swp-cert-dates>Udløber: 1. marts 2025</swp-cert-dates>
</swp-cert-info>
<swp-cert-status class="expiring">Udløber snart</swp-cert-status>
</swp-cert-item>
</swp-cert-list>
<swp-add-button>
+ Tilføj certificering
</swp-add-button>
</swp-card>
<!-- Kurser -->
<swp-card>
<swp-section-label>Kurser</swp-section-label>
<swp-course-section>
<swp-course-section-title>Gennemførte kurser</swp-course-section-title>
<swp-course-list>
<swp-course-item>
<swp-course-info>
<swp-course-name>Avanceret balayage teknikker</swp-course-name>
<swp-course-meta>Wella Academy · Marts 2024</swp-course-meta>
</swp-course-info>
</swp-course-item>
<swp-course-item>
<swp-course-info>
<swp-course-name>Kundeservice & mersalg</swp-course-name>
<swp-course-meta>SalonUp · November 2023</swp-course-meta>
</swp-course-info>
</swp-course-item>
</swp-course-list>
</swp-course-section>
<swp-course-section>
<swp-course-section-title>Planlagte kurser</swp-course-section-title>
<swp-course-list>
<swp-course-item class="upcoming">
<swp-course-info>
<swp-course-name>Olaplex certificering</swp-course-name>
<swp-course-meta>Olaplex DK · 15. februar 2026</swp-course-meta>
</swp-course-info>
<swp-course-status>Tilmeldt</swp-course-status>
</swp-course-item>
</swp-course-list>
</swp-course-section>
<swp-add-button>
+ Tilføj kursus
</swp-add-button>
</swp-card>
</div>
</div>
<!-- Ferie & Fravær sektion -->
<div class="grid-2" style="margin-top: 24px;">
<swp-card>
<swp-section-label>Ferie-saldo</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Optjente feriedage</swp-edit-label>
<swp-edit-value>25 dage</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Brugte feriedage</swp-edit-label>
<swp-edit-value>12 dage</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Resterende</swp-edit-label>
<swp-edit-value style="font-weight: 600; color: var(--color-teal);">13 dage</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
<swp-card>
<swp-section-label>Fravær & Sygdom</swp-section-label>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Sygefravær 2025</swp-edit-label>
<swp-edit-value>3 dage</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Sygefravær 2024</swp-edit-label>
<swp-edit-value>7 dage</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Børns sygdom 2025</swp-edit-label>
<swp-edit-value>1 dag</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Barsel</swp-edit-label>
<swp-edit-value>— (ingen planlagt)</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card>
</div>
<!-- Planlagt fravær -->
<swp-card style="margin-top: 24px;">
<swp-section-label>Planlagt fravær</swp-section-label>
<swp-vacation-list>
<swp-vacation-item>
<swp-vacation-dates>23. dec 2. jan 2026</swp-vacation-dates>
<swp-vacation-type class="ferie">Ferie</swp-vacation-type>
</swp-vacation-item>
<swp-vacation-item>
<swp-vacation-dates>14. feb 2025</swp-vacation-dates>
<swp-vacation-type class="fri">Fri</swp-vacation-type>
</swp-vacation-item>
<swp-vacation-item>
<swp-vacation-dates>7. apr 11. apr 2025</swp-vacation-dates>
<swp-vacation-type class="ferie">Ferie</swp-vacation-type>
</swp-vacation-item>
</swp-vacation-list>
<swp-add-button>
+ Tilføj fravær
</swp-add-button>
</swp-card>
</swp-tab-content>
<!-- ==========================================
STATISTIK TAB
========================================== -->
<swp-tab-content data-tab="stats">
<div class="grid-4" style="margin-bottom: 24px;">
<swp-stat-card class="highlight">
<swp-stat-value>42</swp-stat-value>
<swp-stat-label>Bookinger denne måned</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>30.825 kr</swp-stat-value>
<swp-stat-label>Værdi af bookede services</swp-stat-label>
<swp-stat-subtitle>Baseret på 49 bookinger</swp-stat-subtitle>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>28.450 kr</swp-stat-value>
<swp-stat-label>Omsætning denne måned</swp-stat-label>
</swp-stat-card>
<swp-stat-card>
<swp-stat-value>68%</swp-stat-value>
<swp-stat-label>Gengangere</swp-stat-label>
</swp-stat-card>
</div>
<!-- Omsætning & Belægningsgrad chart -->
<swp-card style="margin-bottom: 24px;">
<swp-chart-section>
<swp-chart-header>
<swp-chart-title>Omsætning & Belægningsgrad</swp-chart-title>
<swp-chart-subtitle>3 måneder bagud · 3 måneder frem</swp-chart-subtitle>
</swp-chart-header>
<swp-chart-container id="revenueUtilizationChart" style="height: 300px;"></swp-chart-container>
</swp-chart-section>
</swp-card>
<div class="grid-2">
<swp-card>
<swp-chart-section>
<swp-chart-header>
<swp-chart-title>Omsætning (sidste 6 mdr)</swp-chart-title>
<swp-chart-legend>
<swp-chart-legend-item>
<swp-chart-legend-dot class="services"></swp-chart-legend-dot>
<span>Services</span>
</swp-chart-legend-item>
<swp-chart-legend-item>
<swp-chart-legend-dot class="products"></swp-chart-legend-dot>
<span>Produkter</span>
</swp-chart-legend-item>
</swp-chart-legend>
</swp-chart-header>
<swp-chart-container id="revenueChart" style="height: 220px;"></swp-chart-container>
</swp-chart-section>
</swp-card>
<swp-card>
<swp-section-label>Seneste bookinger</swp-section-label>
<swp-booking-table>
<table>
<thead>
<tr>
<th>Kunde</th>
<th>Service</th>
<th>Dato</th>
<th>Beløb</th>
</tr>
</thead>
<tbody>
<tr>
<td class="customer">Maria Hansen</td>
<td>Klip & Farve</td>
<td class="date">23. dec 2024</td>
<td class="amount">995 kr</td>
</tr>
<tr>
<td class="customer">Louise Nielsen</td>
<td>Balayage</td>
<td class="date">22. dec 2024</td>
<td class="amount">1.495 kr</td>
</tr>
<tr>
<td class="customer">Sofie Andersen</td>
<td>Dameklip</td>
<td class="date">22. dec 2024</td>
<td class="amount">425 kr</td>
</tr>
<tr>
<td class="customer">Karen Pedersen</td>
<td>Klip & Farve</td>
<td class="date">21. dec 2024</td>
<td class="amount">1.095 kr</td>
</tr>
<tr>
<td class="customer">Emma Larsen</td>
<td>Olaplex</td>
<td class="date">21. dec 2024</td>
<td class="amount">350 kr</td>
</tr>
</tbody>
</table>
</swp-booking-table>
</swp-card>
</div>
<!-- Afsluttede bookinger (fakturaliste) -->
<swp-card style="margin-top: 24px;">
<swp-section-label>Afsluttede bookinger</swp-section-label>
<swp-invoice-table>
<table>
<thead>
<tr>
<th>Dato</th>
<th>Tid</th>
<th>Kunde</th>
<th>Services</th>
<th>Varighed</th>
<th class="amount-col">Beløb</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td class="date">23. dec 2024</td>
<td class="time">10:00</td>
<td class="customer">Maria Hansen</td>
<td class="services">Dameklip, Bundfarve</td>
<td class="duration">2t 30m</td>
<td class="amount">1.510 kr</td>
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
</tr>
<tr>
<td class="date">23. dec 2024</td>
<td class="time">13:30</td>
<td class="customer">Louise Nielsen</td>
<td class="services">Balayage langt hår, Olaplex</td>
<td class="duration">3t</td>
<td class="amount">2.700 kr</td>
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
</tr>
<tr>
<td class="date">22. dec 2024</td>
<td class="time">09:00</td>
<td class="customer">Sofie Andersen</td>
<td class="services">Dameklip</td>
<td class="duration">1t</td>
<td class="amount">725 kr</td>
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
</tr>
<tr>
<td class="date">22. dec 2024</td>
<td class="time">11:00</td>
<td class="customer">Karen Pedersen</td>
<td class="services">Striber mellemlangt hår, Klip</td>
<td class="duration">2t 30m</td>
<td class="amount">2.390 kr</td>
<td><swp-status-badge class="pending">Afventer</swp-status-badge></td>
</tr>
<tr>
<td class="date">21. dec 2024</td>
<td class="time">14:00</td>
<td class="customer">Emma Larsen</td>
<td class="services">Olaplex Stand alone</td>
<td class="duration">1t</td>
<td class="amount">550 kr</td>
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
</tr>
<tr>
<td class="date">21. dec 2024</td>
<td class="time">10:00</td>
<td class="customer">Mette Kristensen</td>
<td class="services">Herreklip</td>
<td class="duration">1t</td>
<td class="amount">645 kr</td>
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
</tr>
<tr>
<td class="date">20. dec 2024</td>
<td class="time">09:30</td>
<td class="customer">Anne Thomsen</td>
<td class="services">Glossing mellemlangt hår</td>
<td class="duration">1t</td>
<td class="amount">745 kr</td>
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
</tr>
<tr>
<td class="date">20. dec 2024</td>
<td class="time">12:00</td>
<td class="customer">Lise Mortensen</td>
<td class="services">Dameklip, Farvning vipper & bryn</td>
<td class="duration">1t 30m</td>
<td class="amount">1.070 kr</td>
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
</tr>
</tbody>
</table>
</swp-invoice-table>
</swp-card>
</swp-tab-content>
<script type="module">
// Tab switching
document.querySelectorAll('swp-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('swp-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('swp-tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.querySelector(`swp-tab-content[data-tab="${tab.dataset.tab}"]`).classList.add('active');
});
});
// Toggle sliders
document.querySelectorAll('swp-toggle-slider').forEach(slider => {
slider.addEventListener('click', () => {
slider.dataset.value = slider.dataset.value === 'yes' ? 'no' : 'yes';
});
});
// ==========================================
// Services Drag-Drop Functionality
// ==========================================
// Category colors
const categoryColors = {
'Klip dame, herre og børn': '#1e88e5',
'Farvebehandlinger': '#8e24aa',
'Striber/ Refleksbehandling': '#f4511e',
'Hårvask med styling eller uden styling': '#00897b',
'Henna naturlig hårfarver': '#6d4c41',
'Kurbehandling': '#43a047',
'Bryn og vipper': '#ec407a',
'Balayage': '#ab47bc',
'Skæg': '#5c6bc0',
'Gloss': '#ffb300',
'Håropsætning': '#e91e63',
'Modeller': '#78909c',
'Tristan farve modeller': '#26a69a',
'Uden kategori': '#607d8b',
'Tilvalg services': '#009688'
};
// Pre-selected services (by name)
const preSelectedServices = [
// Klip
'Dameklip',
'Dameklip uden snak',
'Herreklip',
'Herreklip uden snak',
'Skin fade',
'Børneklip 0-4 år',
'Børneklip 4-8 år',
// Farve
'Bundfarve almindelig udgroning maks 3 cm',
'Helfarve kort hår',
'Helfarve mellemlangt hår',
// Striber
'Striber kort hår',
'Striber mellemlangt hår',
// Bryn og vipper
'Farvning vipper & bryn',
'Farvning vipper',
'Farvning og retning af bryn',
// Balayage
'Balayage maks til skulderen',
'Balayage langt hår',
// Gloss
'Glossing kort hår',
'Glossing mellemlangt/ langt hår'
];
// All services data - loaded from JSON
let allServices = [];
const serviceState = new Map();
const selectedContainer = document.getElementById('selectedServices');
const availableContainer = document.getElementById('availableServices');
const serviceSearch = document.getElementById('serviceSearch');
// Load services from JSON
async function loadServices() {
try {
const response = await fetch('data/kk-services.json');
const data = await response.json();
let id = 1;
for (const [category, services] of Object.entries(data)) {
const color = categoryColors[category] || '#607d8b';
services.forEach(service => {
const isPreSelected = preSelectedServices.includes(service.name);
allServices.push({
id: id,
name: service.name,
category: category,
color: color,
duration: service.duration,
price: service.price,
selected: isPreSelected
});
serviceState.set(id, {
selected: isPreSelected,
priceOverride: null,
durationOverride: 0
});
id++;
});
}
console.log(`Loaded ${allServices.length} services from JSON`);
renderServices();
} catch (error) {
console.error('Fejl ved indlæsning af services:', error);
}
}
// Format duration
function formatDuration(minutes) {
if (minutes >= 60) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return mins > 0 ? `${hours}t ${mins}m` : `${hours}t`;
}
return `${minutes}m`;
}
// Format duration with offset (e.g., "1t -5m" or "30m +10m")
function formatDurationWithOffset(baseMinutes, offset) {
const baseStr = formatDuration(baseMinutes);
if (offset === 0) return baseStr;
const sign = offset > 0 ? '+' : '';
return `${baseStr} <span class="duration-offset">${sign}${offset}m</span>`;
}
// Create service row HTML
function createServiceRow(service, isSelected) {
const state = serviceState.get(service.id);
const effectivePrice = state.priceOverride !== null ? state.priceOverride : service.price;
const effectiveDuration = service.duration + state.durationOverride;
const hasPriceOverride = state.priceOverride !== null && state.priceOverride !== service.price;
const hasDurationOverride = state.durationOverride !== 0;
const row = document.createElement('swp-service-row');
row.dataset.id = service.id;
row.dataset.category = service.category;
row.draggable = true;
if (hasPriceOverride || hasDurationOverride) {
row.classList.add('has-override');
}
if (isSelected) {
// Selected: show adjusted values + original values in bordered box when modified
const hasAnyOverride = hasPriceOverride || hasDurationOverride;
// Original values box (shown when any override exists)
const originalsHtml = hasAnyOverride
? `<swp-service-originals>
<span class="original-price-val ${hasPriceOverride ? 'struck' : ''}" data-original-price="${service.price}">${service.price} kr</span>
<span class="original-duration-val ${hasDurationOverride ? 'struck' : ''}" data-original-duration="${service.duration}">${formatDuration(service.duration)}</span>
</swp-service-originals>`
: '';
row.innerHTML = `
<swp-drag-handle>⋮⋮</swp-drag-handle>
<swp-service-color style="background: ${service.color};"></swp-service-color>
<swp-service-info>
<swp-service-name>${service.name}</swp-service-name>
</swp-service-info>
<swp-service-values>
${originalsHtml}
<swp-service-price class="${hasPriceOverride ? 'modified' : ''}">${effectivePrice} kr</swp-service-price>
<swp-service-duration class="${hasDurationOverride ? 'modified' : ''}">${formatDuration(effectiveDuration)}</swp-service-duration>
</swp-service-values>
<swp-service-controls>
<swp-service-originals-edit>
<span class="original-price-val ${hasPriceOverride ? 'struck' : ''}">${service.price} kr</span>
<span class="original-duration-val ${hasDurationOverride ? 'struck' : ''}">${formatDuration(service.duration)}</span>
</swp-service-originals-edit>
<swp-service-final-values>
<swp-adjustable-value class="price-adjust ${hasPriceOverride ? 'modified' : ''}" data-type="price" data-original="${service.price}" data-value="${effectivePrice}" data-step="5">
<button class="adjust-down" type="button"></button>
<span class="adjust-display">${effectivePrice} kr</span>
<button class="adjust-up" type="button">+</button>
</swp-adjustable-value>
<swp-adjustable-value class="duration-adjust ${hasDurationOverride ? 'modified' : ''}" data-type="duration" data-base="${service.duration}" data-offset="${state.durationOverride}" data-step="5">
<button class="adjust-down" type="button"></button>
<span class="adjust-display">${formatDuration(effectiveDuration)}</span>
<button class="adjust-up" type="button">+</button>
</swp-adjustable-value>
</swp-service-final-values>
</swp-service-controls>
`;
} else {
// Available: name only (no price/duration)
row.innerHTML = `
<swp-drag-handle>⋮⋮</swp-drag-handle>
<swp-service-color style="background: ${service.color};"></swp-service-color>
<swp-service-info>
<swp-service-name>${service.name}</swp-service-name>
</swp-service-info>
`;
}
return row;
}
// Render all services
function renderServices(searchQuery = '') {
const lowerQuery = searchQuery.toLowerCase();
// Group services by category
const selectedByCategory = {};
const availableByCategory = {};
allServices.forEach(service => {
const state = serviceState.get(service.id);
const matchesSearch = !searchQuery || service.name.toLowerCase().includes(lowerQuery);
if (state.selected) {
if (!selectedByCategory[service.category]) {
selectedByCategory[service.category] = [];
}
selectedByCategory[service.category].push(service);
} else if (matchesSearch) {
if (!availableByCategory[service.category]) {
availableByCategory[service.category] = [];
}
availableByCategory[service.category].push(service);
}
});
// Render selected services
selectedContainer.innerHTML = '';
const selectedCategories = Object.entries(selectedByCategory);
if (selectedCategories.length === 0) {
selectedContainer.innerHTML = `
<swp-services-empty>
<svg viewBox="0 0 24 24" width="32" height="32"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Træk services hertil</span>
</swp-services-empty>
`;
} else {
selectedCategories.forEach(([category, services]) => {
const group = document.createElement('swp-service-category-group');
group.innerHTML = `<swp-service-category-header>${category}</swp-service-category-header>`;
services.forEach(service => {
group.appendChild(createServiceRow(service, true));
});
selectedContainer.appendChild(group);
});
}
// Render available services
availableContainer.innerHTML = '';
const availableCategories = Object.entries(availableByCategory);
availableCategories.forEach(([category, services]) => {
const group = document.createElement('swp-service-category-group');
group.innerHTML = `<swp-service-category-header>${category}</swp-service-category-header>`;
services.forEach(service => {
group.appendChild(createServiceRow(service, false));
});
availableContainer.appendChild(group);
});
}
// Multi-selection state
let selectedServiceIds = new Set();
let lastClickedId = null;
// Get all service IDs in order (for shift-selection)
function getOrderedServiceIds(container) {
return Array.from(container.querySelectorAll('swp-service-row'))
.map(row => parseInt(row.dataset.id));
}
// Handle service row click for selection
function handleServiceClick(e, row) {
const serviceId = parseInt(row.dataset.id);
const container = row.closest('swp-services-selected, swp-services-available');
if (e.shiftKey && lastClickedId !== null) {
// Shift+click: range selection
const orderedIds = getOrderedServiceIds(container);
const startIndex = orderedIds.indexOf(lastClickedId);
const endIndex = orderedIds.indexOf(serviceId);
if (startIndex !== -1 && endIndex !== -1) {
const minIndex = Math.min(startIndex, endIndex);
const maxIndex = Math.max(startIndex, endIndex);
for (let i = minIndex; i <= maxIndex; i++) {
selectedServiceIds.add(orderedIds[i]);
}
}
} else if (e.ctrlKey || e.metaKey) {
// Ctrl/Cmd+click: toggle individual selection
if (selectedServiceIds.has(serviceId)) {
selectedServiceIds.delete(serviceId);
} else {
selectedServiceIds.add(serviceId);
}
} else {
// Simple click: single selection
selectedServiceIds.clear();
selectedServiceIds.add(serviceId);
}
lastClickedId = serviceId;
updateSelectionVisual();
}
// Update visual selection state
function updateSelectionVisual() {
document.querySelectorAll('swp-service-row').forEach(row => {
const serviceId = parseInt(row.dataset.id);
if (selectedServiceIds.has(serviceId)) {
row.classList.add('multi-selected');
} else {
row.classList.remove('multi-selected');
}
});
}
// Move service between columns
function moveService(serviceId, toSelected) {
const state = serviceState.get(parseInt(serviceId));
if (state) {
state.selected = toSelected;
renderServices(serviceSearch.value);
}
}
// Move multiple services
function moveMultipleServices(serviceIds, toSelected) {
serviceIds.forEach(id => {
const state = serviceState.get(id);
if (state) {
state.selected = toSelected;
}
});
selectedServiceIds.clear();
renderServices(serviceSearch.value);
}
// Drag and drop handlers
let draggedId = null;
let draggedIds = [];
// Click handler for row selection
document.addEventListener('click', (e) => {
const row = e.target.closest('swp-service-row');
// Don't select on input focus
if (row && e.target.tagName !== 'INPUT') {
handleServiceClick(e, row);
}
});
document.addEventListener('dragstart', (e) => {
const row = e.target.closest('swp-service-row');
if (row) {
const serviceId = parseInt(row.dataset.id);
// If dragging a selected row, drag all selected
if (selectedServiceIds.has(serviceId) && selectedServiceIds.size > 1) {
draggedIds = Array.from(selectedServiceIds);
// Mark all selected as dragging
document.querySelectorAll('swp-service-row.multi-selected').forEach(r => {
r.classList.add('dragging');
});
} else {
// Otherwise, just drag this one
draggedIds = [serviceId];
row.classList.add('dragging');
}
draggedId = row.dataset.id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', draggedIds.join(','));
}
});
document.addEventListener('dragend', (e) => {
document.querySelectorAll('swp-service-row.dragging').forEach(row => {
row.classList.remove('dragging');
});
draggedId = null;
draggedIds = [];
selectedContainer.classList.remove('drag-over');
availableContainer.classList.remove('drag-over');
});
selectedContainer.addEventListener('dragover', (e) => {
e.preventDefault();
selectedContainer.classList.add('drag-over');
});
selectedContainer.addEventListener('dragleave', (e) => {
if (!selectedContainer.contains(e.relatedTarget)) {
selectedContainer.classList.remove('drag-over');
}
});
selectedContainer.addEventListener('drop', (e) => {
e.preventDefault();
selectedContainer.classList.remove('drag-over');
if (draggedIds.length > 0) {
moveMultipleServices(draggedIds, true);
} else if (draggedId) {
moveService(draggedId, true);
}
});
availableContainer.addEventListener('dragover', (e) => {
e.preventDefault();
availableContainer.classList.add('drag-over');
});
availableContainer.addEventListener('dragleave', (e) => {
if (!availableContainer.contains(e.relatedTarget)) {
availableContainer.classList.remove('drag-over');
}
});
availableContainer.addEventListener('drop', (e) => {
e.preventDefault();
availableContainer.classList.remove('drag-over');
if (draggedIds.length > 0) {
moveMultipleServices(draggedIds, false);
} else if (draggedId) {
moveService(draggedId, false);
}
});
// Adjustable value click handler (up/down buttons)
selectedContainer.addEventListener('click', (e) => {
const btn = e.target.closest('.adjust-up, .adjust-down');
if (!btn) return;
const adjustable = btn.closest('swp-adjustable-value');
const row = btn.closest('swp-service-row');
if (!adjustable || !row) return;
const serviceId = parseInt(row.dataset.id);
const state = serviceState.get(serviceId);
const service = allServices.find(s => s.id === serviceId);
const step = parseInt(adjustable.dataset.step);
const isUp = btn.classList.contains('adjust-up');
const delta = isUp ? step : -step;
if (adjustable.dataset.type === 'price') {
// Adjust price
const original = parseInt(adjustable.dataset.original);
let current = parseInt(adjustable.dataset.value);
current = Math.max(0, current + delta);
adjustable.dataset.value = current;
const isModified = current !== original;
state.priceOverride = isModified ? current : null;
// Update displays
adjustable.querySelector('.adjust-display').textContent = `${current} kr`;
adjustable.classList.toggle('modified', isModified);
const priceDisplay = row.querySelector('swp-service-price');
if (priceDisplay) {
priceDisplay.textContent = `${current} kr`;
priceDisplay.classList.toggle('modified', isModified);
}
// Toggle strikethrough on all original price elements (read + edit mode)
row.querySelectorAll('.original-price-val').forEach(el => {
el.classList.toggle('struck', isModified);
});
} else if (adjustable.dataset.type === 'duration') {
// Adjust duration offset
const base = parseInt(adjustable.dataset.base);
let offset = parseInt(adjustable.dataset.offset);
offset = Math.max(-base, offset + delta); // Don't go below 0 total
adjustable.dataset.offset = offset;
const effectiveDuration = base + offset;
const isModified = offset !== 0;
state.durationOverride = offset;
// Update displays
adjustable.querySelector('.adjust-display').textContent = formatDuration(effectiveDuration);
adjustable.classList.toggle('modified', isModified);
const durationDisplay = row.querySelector('swp-service-duration');
if (durationDisplay) {
durationDisplay.textContent = formatDuration(effectiveDuration);
durationDisplay.classList.toggle('modified', isModified);
}
// Toggle strikethrough on all original duration elements (read + edit mode)
row.querySelectorAll('.original-duration-val').forEach(el => {
el.classList.toggle('struck', isModified);
});
}
// Check if any override exists
const hasOverride = state.priceOverride !== null || state.durationOverride !== 0;
row.classList.toggle('has-override', hasOverride);
// Handle originals box visibility and highlight
const valuesContainer = row.querySelector('swp-service-values');
let originalsBox = valuesContainer.querySelector('swp-service-originals');
const originalsEditBox = row.querySelector('swp-service-originals-edit');
if (hasOverride) {
// Show originals box if not present (insert at beginning)
const hasPriceOverride = state.priceOverride !== null;
const hasDurationOverride = state.durationOverride !== 0;
if (!originalsBox) {
originalsBox = document.createElement('swp-service-originals');
originalsBox.innerHTML = `
<span class="original-price-val ${hasPriceOverride ? 'struck' : ''}">${service.price} kr</span>
<span class="original-duration-val ${hasDurationOverride ? 'struck' : ''}">${formatDuration(service.duration)}</span>
`;
valuesContainer.insertBefore(originalsBox, valuesContainer.firstChild);
} else {
// Update struck classes on existing originals box
const priceVal = originalsBox.querySelector('.original-price-val');
const durationVal = originalsBox.querySelector('.original-duration-val');
if (priceVal) priceVal.classList.toggle('struck', hasPriceOverride);
if (durationVal) durationVal.classList.toggle('struck', hasDurationOverride);
}
// Highlight animation
originalsBox.classList.remove('highlight');
originalsEditBox?.classList.remove('highlight');
void originalsBox.offsetWidth; // Trigger reflow
originalsBox.classList.add('highlight');
originalsEditBox?.classList.add('highlight');
} else {
// Remove originals box if no overrides
if (originalsBox) originalsBox.remove();
}
});
// Search handler
let searchTimeout;
serviceSearch.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
renderServices(e.target.value);
}, 150);
});
// Initial load from JSON
loadServices();
// Services panel toggle
const servicesPanel = document.getElementById('servicesPanel');
const panelToggle = document.getElementById('panelToggle');
const editServicesBtn = document.getElementById('editServicesBtn');
const selectedServicesCard = document.querySelector('.selected-services-card');
function toggleServicesPanel() {
const isCollapsed = servicesPanel.classList.contains('collapsed');
servicesPanel.classList.toggle('collapsed', !isCollapsed);
servicesPanel.classList.toggle('expanded', isCollapsed);
selectedServicesCard.classList.toggle('edit-mode', isCollapsed);
editServicesBtn.classList.toggle('active', isCollapsed);
editServicesBtn.innerHTML = isCollapsed
? `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> Luk`
: `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg> Rediger`;
}
panelToggle.addEventListener('click', toggleServicesPanel);
editServicesBtn.addEventListener('click', toggleServicesPanel);
// Checkbox rows
document.querySelectorAll('swp-checkbox-row').forEach(row => {
row.addEventListener('click', () => {
row.classList.toggle('checked');
});
});
// Employee status toggle
const statusEl = document.querySelector('swp-employee-status');
statusEl.addEventListener('click', () => {
const isActive = statusEl.dataset.active === 'true';
statusEl.dataset.active = isActive ? 'false' : 'true';
statusEl.querySelector('.text').textContent = isActive ? 'Inaktiv' : 'Aktiv';
});
// Avatar upload
const avatarUpload = document.getElementById('avatarUpload');
const avatarInput = document.getElementById('avatarInput');
avatarUpload.addEventListener('click', () => {
avatarInput.click();
});
avatarInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (event) => {
// Check if image already exists
let img = avatarUpload.querySelector('img');
if (!img) {
img = document.createElement('img');
avatarUpload.insertBefore(img, avatarUpload.firstChild);
}
img.src = event.target.result;
avatarUpload.classList.add('has-image');
};
reader.readAsDataURL(file);
}
});
// Avatar remove
const avatarRemove = document.getElementById('avatarRemove');
avatarRemove.addEventListener('click', (e) => {
e.stopPropagation();
const img = avatarUpload.querySelector('img');
if (img) {
img.remove();
}
avatarUpload.classList.remove('has-image');
avatarInput.value = '';
});
// Revenue chart - line (Services + Produkter)
import { createChart } from 'https://unpkg.com/@sevenweirdpeople/swp-charting@latest/dist/index.js';
createChart(document.getElementById('revenueChart'), {
xAxis: { categories: ['Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dec'] },
series: [
{
name: 'Services',
color: '#00897b',
data: [
{ x: 'Jul', y: 22000 },
{ x: 'Aug', y: 28500 },
{ x: 'Sep', y: 26200 },
{ x: 'Okt', y: 30500 },
{ x: 'Nov', y: 27100 },
{ x: 'Dec', y: 26000 },
],
showArea: true,
area: { gradient: { startOpacity: 0.3, endOpacity: 0.05 } }
},
{
name: 'Produkter',
color: '#1976d2',
data: [
{ x: 'Jul', y: 2500 },
{ x: 'Aug', y: 2700 },
{ x: 'Sep', y: 2600 },
{ x: 'Okt', y: 2600 },
{ x: 'Nov', y: 2500 },
{ x: 'Dec', y: 2450 },
],
showArea: true,
area: { gradient: { startOpacity: 0.3, endOpacity: 0.05 } }
}
],
yAxis: { format: (v) => (v/1000).toFixed(0) + 'k' },
height: 200,
legend: false
});
// Revenue & Utilization chart - dual axis (3 mdr bagud + 3 mdr frem)
fetch('data/employee-revenue-utilization.json')
.then(res => res.json())
.then(data => {
createChart(document.getElementById('revenueUtilizationChart'), {
xAxis: { categories: data.categories },
yAxis: [
{ min: 0, max: 50000, format: v => `${(v/1000).toFixed(0)}k` }, // Left: Revenue
{ min: 0, max: 100, format: v => `${v}%` }, // Right: Utilization
],
series: [
// Actual revenue (solid bars)
{
name: 'Omsætning',
color: '#3b82f6',
type: 'bar',
yAxisIndex: 0,
unit: 'kr',
data: data.actual.revenue,
bar: { radius: 2 },
},
// Forecast revenue (transparent bars)
{
name: 'Omsætning (forecast)',
color: '#3b82f6',
type: 'bar',
yAxisIndex: 0,
unit: 'kr',
data: data.forecast.revenue,
bar: { radius: 2, opacity: 0.35 },
},
// Actual utilization (solid line)
{
name: 'Belægning',
color: '#00897b',
type: 'line',
yAxisIndex: 1,
unit: '%',
data: data.actual.utilization,
line: { width: 2.5 },
point: { radius: 0 },
showArea: false,
},
// Forecast utilization (dashed line)
{
name: 'Belægning (forecast)',
color: '#00897b',
type: 'line',
yAxisIndex: 1,
unit: '%',
data: data.forecast.utilization,
line: { width: 2.5, dashArray: '4 4' },
point: { radius: 0 },
showArea: false,
},
],
annotations: [
{ type: 'region', x: data.forecastStartCategory, x2: data.categories[data.categories.length - 1], backgroundColor: 'rgba(0,0,0,0.03)' },
{ type: 'verticalLine', x: data.nowCategory, dashArray: '4 4', label: 'Nu', labelPosition: 'top' },
],
legend: { position: 'top', align: 'end' },
padding: { right: 60 },
height: 280,
});
});
</script>
<!-- Rates Drawer -->
<swp-drawer-overlay id="rates-overlay" onclick="closeRatesDrawer()"></swp-drawer-overlay>
<swp-rates-drawer id="rates-drawer">
<swp-drawer-header>
<swp-drawer-title>Lønsatser</swp-drawer-title>
<swp-drawer-close onclick="closeRatesDrawer()">
<i class="ph ph-x"></i>
</swp-drawer-close>
</swp-drawer-header>
<swp-drawer-body>
<swp-section-label style="margin-bottom: 12px;">Grundsatser</swp-section-label>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>Normal (timeløn)</swp-rate-label>
<swp-rate-input><input type="text" value="131,49"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>Overarbejde (100%)</swp-rate-label>
<swp-rate-input><input type="text" value="280,50"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox">
<swp-rate-label class="disabled">Kursus/skole</swp-rate-label>
<swp-rate-input class="disabled"><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox">
<swp-rate-label class="disabled">Afspadsering</swp-rate-label>
<swp-rate-input class="disabled"><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox">
<swp-rate-label class="disabled">Fri m. løn</swp-rate-label>
<swp-rate-input class="disabled"><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>Ferie m. løn</swp-rate-label>
<swp-rate-input><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox">
<swp-rate-label class="disabled">Kontor</swp-rate-label>
<swp-rate-input class="disabled"><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox">
<swp-rate-label class="disabled">Barns 1. sygedag</swp-rate-label>
<swp-rate-input class="disabled"><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox">
<swp-rate-label class="disabled">Barns hospitalsindlæggelse</swp-rate-label>
<swp-rate-input class="disabled"><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox">
<swp-rate-label class="disabled">Barsel</swp-rate-label>
<swp-rate-input class="disabled"><input type="text" value="140,25"> kr</swp-rate-input>
</swp-rate-row>
<div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--color-border);">
<swp-section-label style="margin-bottom: 12px;">Tillæg</swp-section-label>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>8-21 Hverdage (udenfor arbejdstid)</swp-rate-label>
<swp-rate-input><input type="text" value="28,03"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>8-21 Lørdage (udenfor arbejdstid)</swp-rate-label>
<swp-rate-input><input type="text" value="56,02"> kr</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>Søndag</swp-rate-label>
<swp-rate-input><input type="text" value="112,07"> kr</swp-rate-input>
</swp-rate-row>
</div>
<div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--color-border);">
<swp-section-label style="margin-bottom: 12px;">Provisionsberegning</swp-section-label>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>Provision på produktsalg</swp-rate-label>
<swp-rate-input><input type="text" value="15"> %</swp-rate-input>
</swp-rate-row>
<swp-rate-row>
<input type="checkbox" checked>
<swp-rate-label>Provision på servicesalg</swp-rate-label>
<swp-rate-input><input type="text" value="15"> %</swp-rate-input>
</swp-rate-row>
</div>
</swp-drawer-body>
</swp-rates-drawer>
<script>
function openRatesDrawer() {
document.getElementById('rates-overlay').classList.add('open');
document.getElementById('rates-drawer').classList.add('open');
}
function closeRatesDrawer() {
document.getElementById('rates-overlay').classList.remove('open');
document.getElementById('rates-drawer').classList.remove('open');
}
</script>
</body>
</html>