Implements AI-driven time slot selection algorithm for booking system Adds intelligent slot scoring mechanism that considers: - Minimizing calendar gaps - Optimizing employee time utilization - Providing recommended time slots for customers Introduces new AI features across booking interfaces to improve scheduling efficiency
1525 lines
47 KiB
HTML
1525 lines
47 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="da">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Løn & Provision</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css">
|
||
<script src="https://cdn.sheetjs.com/xlsx-0.20.1/package/dist/xlsx.full.min.js"></script>
|
||
<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
|
||
========================================== */
|
||
: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-page-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
swp-topbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* ==========================================
|
||
LAYOUT
|
||
========================================== */
|
||
swp-page-container {
|
||
display: block;
|
||
max-width: 1100px;
|
||
margin: 0 auto;
|
||
padding: 24px;
|
||
}
|
||
|
||
/* ==========================================
|
||
BUTTONS
|
||
========================================== */
|
||
swp-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 18px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
font-family: var(--font-family);
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 150ms ease;
|
||
border: none;
|
||
}
|
||
|
||
swp-btn svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
fill: currentColor;
|
||
}
|
||
|
||
swp-btn.primary {
|
||
background: var(--color-teal);
|
||
color: white;
|
||
}
|
||
|
||
swp-btn.primary:hover {
|
||
background: #00796b;
|
||
}
|
||
|
||
swp-btn.secondary {
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-border);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-btn.secondary:hover {
|
||
background: var(--color-background-hover);
|
||
}
|
||
|
||
swp-btn.small {
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* ==========================================
|
||
CARDS
|
||
========================================== */
|
||
swp-card {
|
||
display: block;
|
||
background: var(--color-surface);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--color-border);
|
||
margin-bottom: 20px;
|
||
padding: 20px;
|
||
}
|
||
|
||
swp-card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin: -20px -20px 20px -20px;
|
||
padding: 14px 20px;
|
||
border-bottom: 1px solid var(--color-border);
|
||
}
|
||
|
||
swp-card-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
/* ==========================================
|
||
PERIODE SELECTOR
|
||
========================================== */
|
||
swp-period-selector {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
swp-period-field {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
swp-period-label {
|
||
font-size: 13px;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
swp-period-field select {
|
||
padding: 8px 12px;
|
||
font-size: 14px;
|
||
font-family: var(--font-family);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
background: var(--color-surface);
|
||
cursor: pointer;
|
||
}
|
||
|
||
swp-period-field select:focus {
|
||
outline: none;
|
||
border-color: var(--color-teal);
|
||
}
|
||
|
||
swp-period-separator {
|
||
font-size: 13px;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
/* ==========================================
|
||
EMPLOYEE ACCORDION
|
||
========================================== */
|
||
swp-employee-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
}
|
||
|
||
swp-employee-card {
|
||
display: block;
|
||
background: var(--color-surface);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--color-border);
|
||
overflow: hidden;
|
||
}
|
||
|
||
swp-employee-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 20px;
|
||
cursor: pointer;
|
||
transition: background 150ms ease;
|
||
}
|
||
|
||
swp-employee-header:hover {
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
swp-employee-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
swp-employee-avatar {
|
||
width: 44px;
|
||
height: 44px;
|
||
border-radius: 50%;
|
||
background: var(--color-teal);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
swp-employee-avatar.purple { background: var(--color-purple); }
|
||
swp-employee-avatar.blue { background: var(--color-blue); }
|
||
swp-employee-avatar.amber { background: var(--color-amber); }
|
||
|
||
swp-employee-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
swp-employee-name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-employee-meta {
|
||
font-size: 12px;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
swp-employee-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
swp-summary-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 2px;
|
||
}
|
||
|
||
swp-summary-value {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
font-family: var(--font-mono);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-summary-label {
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
swp-expand-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--color-text-secondary);
|
||
transition: transform 200ms ease;
|
||
}
|
||
|
||
swp-expand-icon svg {
|
||
width: 20px;
|
||
height: 20px;
|
||
fill: currentColor;
|
||
}
|
||
|
||
swp-employee-card.expanded swp-expand-icon {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
swp-employee-content {
|
||
display: none;
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
|
||
swp-employee-card.expanded swp-employee-content {
|
||
display: block;
|
||
}
|
||
|
||
/* ==========================================
|
||
EMPLOYEE CONFIG
|
||
========================================== */
|
||
swp-employee-config {
|
||
display: flex;
|
||
gap: 24px;
|
||
padding: 16px 20px;
|
||
background: var(--color-background-alt);
|
||
border-bottom: 1px solid var(--color-border);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
swp-config-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
swp-config-label {
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
swp-config-value {
|
||
font-weight: 500;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-config-value.mono {
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
/* ==========================================
|
||
WEEKS TABLE
|
||
========================================== */
|
||
swp-weeks-table {
|
||
display: block;
|
||
padding: 20px;
|
||
}
|
||
|
||
swp-table {
|
||
display: table;
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
swp-table-header {
|
||
display: table-header-group;
|
||
}
|
||
|
||
swp-table-body {
|
||
display: table-row-group;
|
||
}
|
||
|
||
swp-table-footer {
|
||
display: table-footer-group;
|
||
}
|
||
|
||
swp-table-row {
|
||
display: table-row;
|
||
}
|
||
|
||
swp-table-cell {
|
||
display: table-cell;
|
||
padding: 12px 10px;
|
||
font-size: 13px;
|
||
border-bottom: 1px solid var(--color-border);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
swp-table-header swp-table-cell {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
color: var(--color-text-secondary);
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
swp-table-footer swp-table-cell {
|
||
font-weight: 600;
|
||
background: var(--color-background-alt);
|
||
border-bottom: none;
|
||
}
|
||
|
||
swp-table-cell.right {
|
||
text-align: right;
|
||
}
|
||
|
||
swp-table-cell.mono {
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
swp-table-cell.highlight {
|
||
color: var(--color-teal);
|
||
}
|
||
|
||
swp-table-cell.warning {
|
||
color: var(--color-amber);
|
||
}
|
||
|
||
swp-table-body swp-table-row:hover {
|
||
background: var(--color-background-hover);
|
||
}
|
||
|
||
/* Editable cells */
|
||
swp-table-cell input {
|
||
width: 80px;
|
||
padding: 6px 8px;
|
||
font-size: 13px;
|
||
font-family: var(--font-mono);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 4px;
|
||
text-align: right;
|
||
background: var(--color-surface);
|
||
}
|
||
|
||
swp-table-cell input:focus {
|
||
outline: none;
|
||
border-color: var(--color-teal);
|
||
}
|
||
|
||
/* ==========================================
|
||
INTECT BOX
|
||
========================================== */
|
||
swp-intect-box {
|
||
display: block;
|
||
margin: 0 20px 20px;
|
||
padding: 20px;
|
||
background: linear-gradient(135deg, color-mix(in srgb, var(--color-teal) 8%, white) 0%, color-mix(in srgb, var(--color-blue) 8%, white) 100%);
|
||
border: 2px solid var(--color-teal);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
swp-intect-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
swp-intect-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--color-teal);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
swp-intect-title svg {
|
||
width: 18px;
|
||
height: 18px;
|
||
fill: currentColor;
|
||
}
|
||
|
||
swp-intect-rows {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
swp-intect-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 14px;
|
||
}
|
||
|
||
swp-intect-label {
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-intect-value {
|
||
font-weight: 600;
|
||
font-family: var(--font-mono);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-intect-divider {
|
||
height: 1px;
|
||
background: var(--color-teal);
|
||
opacity: 0.3;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
swp-intect-total {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
swp-intect-total swp-intect-value {
|
||
font-size: 18px;
|
||
color: var(--color-teal);
|
||
}
|
||
|
||
swp-copy-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: var(--color-teal);
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-teal);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 150ms ease;
|
||
}
|
||
|
||
swp-copy-btn:hover {
|
||
background: var(--color-teal);
|
||
color: white;
|
||
}
|
||
|
||
swp-copy-btn svg {
|
||
width: 14px;
|
||
height: 14px;
|
||
fill: currentColor;
|
||
}
|
||
|
||
swp-copy-btn.copied {
|
||
background: var(--color-green);
|
||
border-color: var(--color-green);
|
||
color: white;
|
||
}
|
||
|
||
/* ==========================================
|
||
TOTAL SUMMARY
|
||
========================================== */
|
||
swp-total-summary {
|
||
display: block;
|
||
background: var(--color-surface);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--color-border);
|
||
padding: 24px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
swp-total-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
swp-total-header i {
|
||
font-size: 24px;
|
||
color: var(--color-teal);
|
||
}
|
||
|
||
swp-total-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
swp-total-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(5, 1fr);
|
||
gap: 20px;
|
||
}
|
||
|
||
swp-total-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
padding: 16px;
|
||
background: var(--color-background-alt);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
swp-total-item.highlight {
|
||
background: color-mix(in srgb, var(--color-teal) 10%, white);
|
||
}
|
||
|
||
swp-total-value {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
font-family: var(--font-mono);
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-total-item.highlight swp-total-value {
|
||
color: var(--color-teal);
|
||
}
|
||
|
||
swp-total-label {
|
||
font-size: 12px;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
swp-total-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
/* ==========================================
|
||
SECTION LABEL
|
||
========================================== */
|
||
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;
|
||
}
|
||
|
||
/* ==========================================
|
||
PAYROLL EXPORT
|
||
========================================== */
|
||
swp-export-table {
|
||
display: block;
|
||
margin-bottom: 20px;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
swp-export-row {
|
||
display: grid;
|
||
grid-template-columns: 32px 1fr 90px 80px 55px 85px 85px 100px 44px;
|
||
align-items: center;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--color-border);
|
||
gap: 8px;
|
||
}
|
||
|
||
swp-export-checkbox {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
swp-export-checkbox input[type="checkbox"] {
|
||
width: 18px;
|
||
height: 18px;
|
||
cursor: pointer;
|
||
accent-color: var(--color-teal);
|
||
}
|
||
|
||
swp-export-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
swp-export-row.header {
|
||
background: var(--color-background-alt);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
color: var(--color-text-secondary);
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
swp-export-row.total {
|
||
background: color-mix(in srgb, var(--color-teal) 8%, white);
|
||
font-weight: 600;
|
||
}
|
||
|
||
swp-export-cell {
|
||
font-size: 14px;
|
||
}
|
||
|
||
swp-export-cell.right {
|
||
text-align: right;
|
||
}
|
||
|
||
swp-export-cell.mono {
|
||
font-family: var(--font-mono);
|
||
font-size: 13px;
|
||
}
|
||
|
||
swp-export-cell.highlight {
|
||
color: var(--color-teal);
|
||
font-weight: 600;
|
||
}
|
||
|
||
swp-copy-cell-btn {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: var(--color-background);
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
color: var(--color-text-secondary);
|
||
transition: all 150ms ease;
|
||
}
|
||
|
||
swp-copy-cell-btn:hover {
|
||
background: var(--color-teal);
|
||
color: white;
|
||
}
|
||
|
||
swp-copy-cell-btn.copied {
|
||
background: var(--color-green);
|
||
color: white;
|
||
}
|
||
|
||
swp-copy-cell-btn svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
fill: currentColor;
|
||
}
|
||
|
||
swp-export-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 12px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--color-border);
|
||
}
|
||
</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
|
||
</swp-back-link>
|
||
<swp-page-title>Løn & Provision</swp-page-title>
|
||
</swp-topbar-left>
|
||
<swp-topbar-right>
|
||
<swp-btn class="secondary">
|
||
<svg viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
||
Eksporter
|
||
</swp-btn>
|
||
</swp-topbar-right>
|
||
</swp-topbar>
|
||
|
||
<swp-page-container>
|
||
<!-- Periode selector -->
|
||
<swp-card>
|
||
<swp-card-header>
|
||
<swp-card-title>Vælg periode</swp-card-title>
|
||
</swp-card-header>
|
||
<swp-period-selector>
|
||
<swp-period-field>
|
||
<swp-period-label>År:</swp-period-label>
|
||
<select id="yearSelect">
|
||
<option value="2025" selected>2025</option>
|
||
<option value="2024">2024</option>
|
||
</select>
|
||
</swp-period-field>
|
||
<swp-period-field>
|
||
<swp-period-label>Fra uge:</swp-period-label>
|
||
<select id="weekFromSelect"></select>
|
||
</swp-period-field>
|
||
<swp-period-separator>—</swp-period-separator>
|
||
<swp-period-field>
|
||
<swp-period-label>Til uge:</swp-period-label>
|
||
<select id="weekToSelect"></select>
|
||
</swp-period-field>
|
||
<swp-btn class="primary" id="loadDataBtn">
|
||
<svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
|
||
Hent data
|
||
</swp-btn>
|
||
</swp-period-selector>
|
||
</swp-card>
|
||
|
||
<!-- Total summary -->
|
||
<swp-total-summary id="totalSummary">
|
||
<swp-total-header>
|
||
<i class="ph ph-coins"></i>
|
||
<swp-total-title>Samlet overblik for perioden</swp-total-title>
|
||
</swp-total-header>
|
||
<swp-total-grid>
|
||
<swp-total-item class="highlight">
|
||
<swp-total-value id="grandTotal">0 kr</swp-total-value>
|
||
<swp-total-label>Total løn</swp-total-label>
|
||
</swp-total-item>
|
||
<swp-total-item>
|
||
<swp-total-value id="totalBasePay">0 kr</swp-total-value>
|
||
<swp-total-label>Grundløn</swp-total-label>
|
||
</swp-total-item>
|
||
<swp-total-item>
|
||
<swp-total-value id="totalOvertime">0 kr</swp-total-value>
|
||
<swp-total-label>Overtidstillæg</swp-total-label>
|
||
</swp-total-item>
|
||
<swp-total-item>
|
||
<swp-total-value id="totalServiceCommission">0 kr</swp-total-value>
|
||
<swp-total-label>Prov. services</swp-total-label>
|
||
</swp-total-item>
|
||
<swp-total-item>
|
||
<swp-total-value id="totalProductCommission">0 kr</swp-total-value>
|
||
<swp-total-label>Prov. produkter</swp-total-label>
|
||
</swp-total-item>
|
||
</swp-total-grid>
|
||
</swp-total-summary>
|
||
|
||
<!-- Employee list -->
|
||
<swp-employee-list id="employeeList">
|
||
<!-- Rendered by JavaScript -->
|
||
</swp-employee-list>
|
||
|
||
<!-- Payroll Export Section -->
|
||
<swp-card id="payrollExportCard">
|
||
<swp-section-label>Løneksport til Intect</swp-section-label>
|
||
|
||
<swp-export-table id="exportTable">
|
||
<!-- Rendered by JavaScript -->
|
||
</swp-export-table>
|
||
|
||
<swp-export-actions>
|
||
<swp-btn class="secondary" id="copyAllBtn">
|
||
<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
||
Kopier alle til clipboard
|
||
</swp-btn>
|
||
<swp-btn class="primary" id="exportBtn">
|
||
<svg viewBox="0 0 24 24"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg>
|
||
Eksporter til Intect
|
||
</swp-btn>
|
||
</swp-export-actions>
|
||
</swp-card>
|
||
</swp-page-container>
|
||
|
||
<script>
|
||
// ==========================================
|
||
// MOCK DATA
|
||
// ==========================================
|
||
const employees = [
|
||
{
|
||
id: 1,
|
||
name: "Emma Larsen",
|
||
initials: "EL",
|
||
cpr: "010190-1234",
|
||
color: "",
|
||
hourlyRate: 185,
|
||
weeklyHours: 37,
|
||
overtimeRate: 1.5,
|
||
commission: {
|
||
hourlyMinimum: 220, // kr pr. time inkl. moms
|
||
servicePercentage: 15, // % på services over minimum
|
||
productPercentage: 15 // % på produktsalg over minimum
|
||
}
|
||
},
|
||
{
|
||
id: 2,
|
||
name: "Camilla Jensen",
|
||
initials: "CJ",
|
||
cpr: "150885-5678",
|
||
color: "purple",
|
||
hourlyRate: 175,
|
||
weeklyHours: 37,
|
||
overtimeRate: 1.5,
|
||
commission: {
|
||
hourlyMinimum: 220,
|
||
servicePercentage: 15,
|
||
productPercentage: 15
|
||
}
|
||
},
|
||
{
|
||
id: 3,
|
||
name: "Sofie Nielsen",
|
||
initials: "SN",
|
||
cpr: "220792-9012",
|
||
color: "blue",
|
||
hourlyRate: 195,
|
||
weeklyHours: 30, // Deltid
|
||
overtimeRate: 1.5,
|
||
commission: {
|
||
hourlyMinimum: 220,
|
||
servicePercentage: 15,
|
||
productPercentage: 15
|
||
}
|
||
}
|
||
];
|
||
|
||
// Generate mock weekly data
|
||
function generateMockData(year, weekFrom, weekTo) {
|
||
const data = [];
|
||
for (const emp of employees) {
|
||
for (let week = weekFrom; week <= weekTo; week++) {
|
||
// Random hours around their weekly hours
|
||
const hours = Math.floor(Math.random() * 10) + emp.weeklyHours - 3;
|
||
// Random service revenue (70-80% of total)
|
||
const serviceRevenue = Math.floor(Math.random() * 15000) + 8000;
|
||
// Random product revenue (20-30% of total)
|
||
const productRevenue = Math.floor(Math.random() * 5000) + 2000;
|
||
// Random vacation days (0-2 per week, mostly 0)
|
||
const vacationDays = Math.random() > 0.8 ? Math.floor(Math.random() * 3) : 0;
|
||
data.push({
|
||
employeeId: emp.id,
|
||
week,
|
||
year,
|
||
hoursWorked: Math.max(hours, 20),
|
||
serviceRevenue,
|
||
productRevenue,
|
||
vacationDays
|
||
});
|
||
}
|
||
}
|
||
return data;
|
||
}
|
||
|
||
let weeklyData = [];
|
||
|
||
// ==========================================
|
||
// CALCULATIONS
|
||
// ==========================================
|
||
function calculateWeekPayroll(employee, weekData) {
|
||
const normalHours = Math.min(weekData.hoursWorked, employee.weeklyHours);
|
||
const overtimeHours = Math.max(0, weekData.hoursWorked - employee.weeklyHours);
|
||
|
||
const normalPay = normalHours * employee.hourlyRate;
|
||
const overtimePay = overtimeHours * employee.hourlyRate * employee.overtimeRate;
|
||
|
||
// Provision: minimum baseret på timer × timesat
|
||
const totalRevenue = weekData.serviceRevenue + weekData.productRevenue;
|
||
const minimumThreshold = weekData.hoursWorked * employee.commission.hourlyMinimum;
|
||
const revenueOverMinimum = Math.max(0, totalRevenue - minimumThreshold);
|
||
|
||
// Fordel overskud proportionalt mellem services og produkter
|
||
let serviceCommission = 0;
|
||
let productCommission = 0;
|
||
|
||
if (revenueOverMinimum > 0 && totalRevenue > 0) {
|
||
const serviceRatio = weekData.serviceRevenue / totalRevenue;
|
||
const productRatio = weekData.productRevenue / totalRevenue;
|
||
|
||
const serviceOverMinimum = revenueOverMinimum * serviceRatio;
|
||
const productOverMinimum = revenueOverMinimum * productRatio;
|
||
|
||
serviceCommission = serviceOverMinimum * (employee.commission.servicePercentage / 100);
|
||
productCommission = productOverMinimum * (employee.commission.productPercentage / 100);
|
||
}
|
||
|
||
const totalCommission = serviceCommission + productCommission;
|
||
|
||
return {
|
||
normalHours,
|
||
overtimeHours,
|
||
normalPay,
|
||
overtimePay,
|
||
serviceRevenue: weekData.serviceRevenue,
|
||
productRevenue: weekData.productRevenue,
|
||
totalRevenue,
|
||
minimumThreshold,
|
||
revenueOverMinimum,
|
||
serviceCommission,
|
||
productCommission,
|
||
commission: totalCommission,
|
||
vacationDays: weekData.vacationDays || 0,
|
||
totalPay: normalPay + overtimePay + totalCommission
|
||
};
|
||
}
|
||
|
||
function calculateEmployeeTotals(employee, weeks) {
|
||
const employeeWeeks = weeks.filter(w => w.employeeId === employee.id);
|
||
|
||
let totalNormalHours = 0;
|
||
let totalOvertimeHours = 0;
|
||
let totalNormalPay = 0;
|
||
let totalOvertimePay = 0;
|
||
let totalServiceCommission = 0;
|
||
let totalProductCommission = 0;
|
||
let totalServiceRevenue = 0;
|
||
let totalProductRevenue = 0;
|
||
let totalVacationDays = 0;
|
||
|
||
const weekResults = [];
|
||
|
||
for (const week of employeeWeeks) {
|
||
const calc = calculateWeekPayroll(employee, week);
|
||
weekResults.push({
|
||
...week,
|
||
...calc
|
||
});
|
||
|
||
totalNormalHours += calc.normalHours;
|
||
totalOvertimeHours += calc.overtimeHours;
|
||
totalNormalPay += calc.normalPay;
|
||
totalOvertimePay += calc.overtimePay;
|
||
totalServiceCommission += calc.serviceCommission;
|
||
totalProductCommission += calc.productCommission;
|
||
totalServiceRevenue += calc.serviceRevenue;
|
||
totalProductRevenue += calc.productRevenue;
|
||
totalVacationDays += calc.vacationDays;
|
||
}
|
||
|
||
const totalCommission = totalServiceCommission + totalProductCommission;
|
||
|
||
return {
|
||
weeks: weekResults,
|
||
totals: {
|
||
normalHours: totalNormalHours,
|
||
overtimeHours: totalOvertimeHours,
|
||
normalPay: totalNormalPay,
|
||
overtimePay: totalOvertimePay,
|
||
serviceCommission: totalServiceCommission,
|
||
productCommission: totalProductCommission,
|
||
commission: totalCommission,
|
||
serviceRevenue: totalServiceRevenue,
|
||
productRevenue: totalProductRevenue,
|
||
revenue: totalServiceRevenue + totalProductRevenue,
|
||
vacationDays: totalVacationDays,
|
||
total: totalNormalPay + totalOvertimePay + totalCommission
|
||
}
|
||
};
|
||
}
|
||
|
||
// ==========================================
|
||
// FORMATTING
|
||
// ==========================================
|
||
function formatCurrency(amount) {
|
||
return Math.round(amount).toLocaleString('da-DK') + ' kr';
|
||
}
|
||
|
||
function formatHours(hours) {
|
||
return hours + 't';
|
||
}
|
||
|
||
// ==========================================
|
||
// RENDER FUNCTIONS
|
||
// ==========================================
|
||
function renderEmployeeCard(employee, data, isExpanded = false) {
|
||
const { weeks, totals } = data;
|
||
|
||
return `
|
||
<swp-employee-card class="${isExpanded ? 'expanded' : ''}" data-employee-id="${employee.id}">
|
||
<swp-employee-header onclick="toggleEmployee(${employee.id})">
|
||
<swp-employee-info>
|
||
<swp-employee-avatar class="${employee.color}">${employee.initials}</swp-employee-avatar>
|
||
<swp-employee-details>
|
||
<swp-employee-name>${employee.name}</swp-employee-name>
|
||
<swp-employee-meta>${formatHours(totals.normalHours + totals.overtimeHours)} · ${formatCurrency(totals.revenue)} oms.</swp-employee-meta>
|
||
</swp-employee-details>
|
||
</swp-employee-info>
|
||
<swp-employee-summary>
|
||
<swp-summary-item>
|
||
<swp-summary-value>${formatCurrency(totals.commission)}</swp-summary-value>
|
||
<swp-summary-label>Provision</swp-summary-label>
|
||
</swp-summary-item>
|
||
<swp-summary-item>
|
||
<swp-summary-value>${formatCurrency(totals.total)}</swp-summary-value>
|
||
<swp-summary-label>Total</swp-summary-label>
|
||
</swp-summary-item>
|
||
<swp-expand-icon>
|
||
<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
|
||
</swp-expand-icon>
|
||
</swp-employee-summary>
|
||
</swp-employee-header>
|
||
<swp-employee-content>
|
||
<swp-employee-config>
|
||
<swp-config-item>
|
||
<swp-config-label>Timeløn:</swp-config-label>
|
||
<swp-config-value class="mono">${employee.hourlyRate} kr</swp-config-value>
|
||
</swp-config-item>
|
||
<swp-config-item>
|
||
<swp-config-label>Normtid:</swp-config-label>
|
||
<swp-config-value>${employee.weeklyHours}t/uge</swp-config-value>
|
||
</swp-config-item>
|
||
<swp-config-item>
|
||
<swp-config-label>Overtid:</swp-config-label>
|
||
<swp-config-value>+${Math.round((employee.overtimeRate - 1) * 100)}%</swp-config-value>
|
||
</swp-config-item>
|
||
<swp-config-item>
|
||
<swp-config-label>Minimum:</swp-config-label>
|
||
<swp-config-value class="mono">${employee.commission.hourlyMinimum} kr/time</swp-config-value>
|
||
</swp-config-item>
|
||
<swp-config-item>
|
||
<swp-config-label>Provision:</swp-config-label>
|
||
<swp-config-value>${employee.commission.servicePercentage}% services · ${employee.commission.productPercentage}% produkter</swp-config-value>
|
||
</swp-config-item>
|
||
</swp-employee-config>
|
||
|
||
<swp-weeks-table>
|
||
<swp-table>
|
||
<swp-table-header>
|
||
<swp-table-row>
|
||
<swp-table-cell>Uge</swp-table-cell>
|
||
<swp-table-cell class="right">Timer</swp-table-cell>
|
||
<swp-table-cell class="right">Overtid</swp-table-cell>
|
||
<swp-table-cell class="right">Ferie</swp-table-cell>
|
||
<swp-table-cell class="right">Services</swp-table-cell>
|
||
<swp-table-cell class="right">Produkter</swp-table-cell>
|
||
<swp-table-cell class="right">Minimum</swp-table-cell>
|
||
<swp-table-cell class="right">Provision</swp-table-cell>
|
||
<swp-table-cell class="right">I alt</swp-table-cell>
|
||
</swp-table-row>
|
||
</swp-table-header>
|
||
<swp-table-body>
|
||
${weeks.map(w => `
|
||
<swp-table-row>
|
||
<swp-table-cell>Uge ${w.week}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatHours(w.normalHours)}</swp-table-cell>
|
||
<swp-table-cell class="right mono ${w.overtimeHours > 0 ? 'warning' : ''}">${formatHours(w.overtimeHours)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${w.vacationDays} dg</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(w.serviceRevenue)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(w.productRevenue)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(w.minimumThreshold)}</swp-table-cell>
|
||
<swp-table-cell class="right mono ${w.commission > 0 ? 'highlight' : ''}">${formatCurrency(w.commission)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(w.totalPay)}</swp-table-cell>
|
||
</swp-table-row>
|
||
`).join('')}
|
||
</swp-table-body>
|
||
<swp-table-footer>
|
||
<swp-table-row>
|
||
<swp-table-cell>TOTAL</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatHours(totals.normalHours)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatHours(totals.overtimeHours)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${totals.vacationDays} dg</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(totals.serviceRevenue)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(totals.productRevenue)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">—</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(totals.commission)}</swp-table-cell>
|
||
<swp-table-cell class="right mono">${formatCurrency(totals.total)}</swp-table-cell>
|
||
</swp-table-row>
|
||
</swp-table-footer>
|
||
</swp-table>
|
||
</swp-weeks-table>
|
||
|
||
</swp-employee-content>
|
||
</swp-employee-card>
|
||
`;
|
||
}
|
||
|
||
function renderEmployeeList() {
|
||
const container = document.getElementById('employeeList');
|
||
|
||
let html = '';
|
||
let grandTotal = 0;
|
||
let totalBasePay = 0;
|
||
let totalOvertime = 0;
|
||
let totalServiceCommission = 0;
|
||
let totalProductCommission = 0;
|
||
|
||
for (const emp of employees) {
|
||
const data = calculateEmployeeTotals(emp, weeklyData);
|
||
html += renderEmployeeCard(emp, data, emp.id === 1);
|
||
|
||
grandTotal += data.totals.total;
|
||
totalBasePay += data.totals.normalPay;
|
||
totalOvertime += data.totals.overtimePay;
|
||
totalServiceCommission += data.totals.serviceCommission;
|
||
totalProductCommission += data.totals.productCommission;
|
||
}
|
||
|
||
container.innerHTML = html;
|
||
|
||
// Update totals
|
||
document.getElementById('grandTotal').textContent = formatCurrency(grandTotal);
|
||
document.getElementById('totalBasePay').textContent = formatCurrency(totalBasePay);
|
||
document.getElementById('totalOvertime').textContent = formatCurrency(totalOvertime);
|
||
document.getElementById('totalServiceCommission').textContent = formatCurrency(totalServiceCommission);
|
||
document.getElementById('totalProductCommission').textContent = formatCurrency(totalProductCommission);
|
||
}
|
||
|
||
// ==========================================
|
||
// INTERACTIONS
|
||
// ==========================================
|
||
function toggleEmployee(employeeId) {
|
||
const card = document.querySelector(`swp-employee-card[data-employee-id="${employeeId}"]`);
|
||
card.classList.toggle('expanded');
|
||
}
|
||
|
||
// ==========================================
|
||
// EXPORT TABLE
|
||
// ==========================================
|
||
let employeeTotalsCache = [];
|
||
|
||
function renderExportTable() {
|
||
const container = document.getElementById('exportTable');
|
||
|
||
let grandTotalBasePay = 0;
|
||
let grandTotalOvertime = 0;
|
||
let grandTotalVacationDays = 0;
|
||
let grandTotalServiceCommission = 0;
|
||
let grandTotalProductCommission = 0;
|
||
let grandTotal = 0;
|
||
|
||
let rows = `
|
||
<swp-export-row class="header">
|
||
<swp-export-checkbox>
|
||
<input type="checkbox" id="selectAllExport" checked onchange="toggleAllExport(this.checked)">
|
||
</swp-export-checkbox>
|
||
<swp-export-cell>Medarbejder</swp-export-cell>
|
||
<swp-export-cell class="right">Grundløn</swp-export-cell>
|
||
<swp-export-cell class="right">Overtid</swp-export-cell>
|
||
<swp-export-cell class="right">Ferie</swp-export-cell>
|
||
<swp-export-cell class="right">Prov. serv.</swp-export-cell>
|
||
<swp-export-cell class="right">Prov. prod.</swp-export-cell>
|
||
<swp-export-cell class="right">Total</swp-export-cell>
|
||
<swp-export-cell></swp-export-cell>
|
||
</swp-export-row>
|
||
`;
|
||
|
||
employeeTotalsCache = [];
|
||
|
||
for (const emp of employees) {
|
||
const data = calculateEmployeeTotals(emp, weeklyData);
|
||
const { totals } = data;
|
||
|
||
employeeTotalsCache.push({ employee: emp, totals });
|
||
|
||
grandTotalBasePay += totals.normalPay;
|
||
grandTotalOvertime += totals.overtimePay;
|
||
grandTotalVacationDays += totals.vacationDays;
|
||
grandTotalServiceCommission += totals.serviceCommission;
|
||
grandTotalProductCommission += totals.productCommission;
|
||
grandTotal += totals.total;
|
||
|
||
rows += `
|
||
<swp-export-row data-employee-id="${emp.id}">
|
||
<swp-export-checkbox>
|
||
<input type="checkbox" class="export-employee-checkbox" data-employee-id="${emp.id}" checked onchange="updateSelectAllCheckbox()">
|
||
</swp-export-checkbox>
|
||
<swp-export-cell>${emp.name}</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(totals.normalPay)}</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(totals.overtimePay)}</swp-export-cell>
|
||
<swp-export-cell class="right mono">${totals.vacationDays} dg</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(totals.serviceCommission)}</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(totals.productCommission)}</swp-export-cell>
|
||
<swp-export-cell class="right mono highlight">${formatCurrency(totals.total)}</swp-export-cell>
|
||
<swp-export-cell>
|
||
<swp-copy-cell-btn onclick="copyEmployeeRow(${emp.id}, event)" title="Kopier">
|
||
<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
||
</swp-copy-cell-btn>
|
||
</swp-export-cell>
|
||
</swp-export-row>
|
||
`;
|
||
}
|
||
|
||
rows += `
|
||
<swp-export-row class="total">
|
||
<swp-export-cell></swp-export-cell>
|
||
<swp-export-cell>TOTAL</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(grandTotalBasePay)}</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(grandTotalOvertime)}</swp-export-cell>
|
||
<swp-export-cell class="right mono">${grandTotalVacationDays} dg</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(grandTotalServiceCommission)}</swp-export-cell>
|
||
<swp-export-cell class="right mono">${formatCurrency(grandTotalProductCommission)}</swp-export-cell>
|
||
<swp-export-cell class="right mono highlight">${formatCurrency(grandTotal)}</swp-export-cell>
|
||
<swp-export-cell></swp-export-cell>
|
||
</swp-export-row>
|
||
`;
|
||
|
||
container.innerHTML = rows;
|
||
}
|
||
|
||
function toggleAllExport(checked) {
|
||
const checkboxes = document.querySelectorAll('.export-employee-checkbox');
|
||
checkboxes.forEach(cb => cb.checked = checked);
|
||
}
|
||
|
||
function updateSelectAllCheckbox() {
|
||
const checkboxes = document.querySelectorAll('.export-employee-checkbox');
|
||
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
||
const someChecked = Array.from(checkboxes).some(cb => cb.checked);
|
||
const selectAll = document.getElementById('selectAllExport');
|
||
selectAll.checked = allChecked;
|
||
selectAll.indeterminate = someChecked && !allChecked;
|
||
}
|
||
|
||
function getSelectedEmployeeIds() {
|
||
const checkboxes = document.querySelectorAll('.export-employee-checkbox:checked');
|
||
return Array.from(checkboxes).map(cb => parseInt(cb.dataset.employeeId));
|
||
}
|
||
|
||
function copyEmployeeRow(employeeId, event) {
|
||
const cached = employeeTotalsCache.find(c => c.employee.id === employeeId);
|
||
if (!cached) return;
|
||
|
||
const { employee, totals } = cached;
|
||
const text = `${employee.name}\t${Math.round(totals.normalPay)}\t${Math.round(totals.overtimePay)}\t${totals.vacationDays}\t${Math.round(totals.serviceCommission)}\t${Math.round(totals.productCommission)}\t${Math.round(totals.total)}`;
|
||
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const btn = event.target.closest('swp-copy-cell-btn');
|
||
btn.classList.add('copied');
|
||
btn.innerHTML = `<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>`;
|
||
|
||
setTimeout(() => {
|
||
btn.classList.remove('copied');
|
||
btn.innerHTML = `<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
|
||
}, 1500);
|
||
});
|
||
}
|
||
|
||
function copyAllToClipboard() {
|
||
let text = "Medarbejder\tGrundløn\tOvertid\tFeriedage\tProv. serv.\tProv. prod.\tTotal\n";
|
||
|
||
for (const { employee, totals } of employeeTotalsCache) {
|
||
text += `${employee.name}\t${Math.round(totals.normalPay)}\t${Math.round(totals.overtimePay)}\t${totals.vacationDays}\t${Math.round(totals.serviceCommission)}\t${Math.round(totals.productCommission)}\t${Math.round(totals.total)}\n`;
|
||
}
|
||
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
const btn = document.getElementById('copyAllBtn');
|
||
const originalHtml = btn.innerHTML;
|
||
btn.innerHTML = `<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> Kopieret!`;
|
||
|
||
setTimeout(() => {
|
||
btn.innerHTML = originalHtml;
|
||
}, 2000);
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// INTECT EXPORT
|
||
// ==========================================
|
||
function generateIntectData() {
|
||
const rows = [];
|
||
const selectedIds = getSelectedEmployeeIds();
|
||
const selectedEmployees = employees.filter(emp => selectedIds.includes(emp.id));
|
||
|
||
for (const emp of selectedEmployees) {
|
||
const data = calculateEmployeeTotals(emp, weeklyData);
|
||
|
||
// Løn - en linje pr. uge (normal tid)
|
||
for (const week of data.weeks) {
|
||
rows.push({
|
||
cpr: emp.cpr || '',
|
||
name: emp.name,
|
||
initials: emp.initials,
|
||
lønart: 'Løn',
|
||
beskrivelse: `Uge ${week.week}`,
|
||
enheder: week.normalHours,
|
||
sats: emp.hourlyRate,
|
||
beløb: week.normalPay
|
||
});
|
||
|
||
// Overtid - også lønart "Løn", pr. uge
|
||
if (week.overtimeHours > 0) {
|
||
rows.push({
|
||
cpr: emp.cpr || '',
|
||
name: emp.name,
|
||
initials: emp.initials,
|
||
lønart: 'Løn',
|
||
beskrivelse: `Uge ${week.week} Overtid`,
|
||
enheder: week.overtimeHours,
|
||
sats: emp.hourlyRate * emp.overtimeRate,
|
||
beløb: week.overtimePay
|
||
});
|
||
}
|
||
}
|
||
|
||
// Provision - en linje pr. uge med provision
|
||
for (const week of data.weeks) {
|
||
if (week.commission > 0) {
|
||
rows.push({
|
||
cpr: emp.cpr || '',
|
||
name: emp.name,
|
||
initials: emp.initials,
|
||
lønart: 'Provision',
|
||
beskrivelse: `Uge ${week.week} provision ${emp.commission.servicePercentage}%, oms ${Math.round(week.totalRevenue)}`,
|
||
grundlag: week.revenueOverMinimum,
|
||
enheder: emp.commission.servicePercentage,
|
||
sats: week.revenueOverMinimum,
|
||
beløb: week.commission
|
||
});
|
||
}
|
||
}
|
||
|
||
// Ferie - altid inkluderet, også ved 0 dage
|
||
rows.push({
|
||
cpr: emp.cpr || '',
|
||
name: emp.name,
|
||
initials: emp.initials,
|
||
lønart: 'Ferie',
|
||
beskrivelse: '',
|
||
enheder: data.totals.vacationDays,
|
||
sats: '',
|
||
beløb: 0
|
||
});
|
||
}
|
||
|
||
return rows;
|
||
}
|
||
|
||
function exportToIntect() {
|
||
const year = document.getElementById('yearSelect').value;
|
||
const weekFrom = document.getElementById('weekFromSelect').value;
|
||
const weekTo = document.getElementById('weekToSelect').value;
|
||
|
||
const data = generateIntectData();
|
||
|
||
// Konverter til worksheet format
|
||
const wsData = [
|
||
['CPR-nummer', 'Medarbejder navn', 'Initialer', 'Ekstern reference',
|
||
'Medarbejder-ID', 'Lønart', 'Beskrivelse', 'Grundlag', 'Enheder',
|
||
'Sats', 'Beløb', 'Efter næste kørsel', 'Fra dato', 'Til dato'],
|
||
...data.map(row => [
|
||
row.cpr, row.name, row.initials, '', '',
|
||
row.lønart, row.beskrivelse, row.grundlag || '',
|
||
row.enheder, row.sats, row.beløb, 'Nulstil', '', ''
|
||
])
|
||
];
|
||
|
||
const wb = XLSX.utils.book_new();
|
||
const ws = XLSX.utils.aoa_to_sheet(wsData);
|
||
XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
|
||
XLSX.writeFile(wb, `loeneksport_${year}_uge${weekFrom}-${weekTo}.xlsx`);
|
||
}
|
||
|
||
// ==========================================
|
||
// INITIALIZATION
|
||
// ==========================================
|
||
function initWeekSelects() {
|
||
const weekFrom = document.getElementById('weekFromSelect');
|
||
const weekTo = document.getElementById('weekToSelect');
|
||
|
||
for (let i = 1; i <= 52; i++) {
|
||
weekFrom.innerHTML += `<option value="${i}" ${i === 1 ? 'selected' : ''}>Uge ${i}</option>`;
|
||
weekTo.innerHTML += `<option value="${i}" ${i === 4 ? 'selected' : ''}>Uge ${i}</option>`;
|
||
}
|
||
}
|
||
|
||
function loadData() {
|
||
const year = parseInt(document.getElementById('yearSelect').value);
|
||
const weekFrom = parseInt(document.getElementById('weekFromSelect').value);
|
||
const weekTo = parseInt(document.getElementById('weekToSelect').value);
|
||
|
||
weeklyData = generateMockData(year, weekFrom, weekTo);
|
||
renderEmployeeList();
|
||
renderExportTable();
|
||
}
|
||
|
||
// Initialize
|
||
initWeekSelects();
|
||
document.getElementById('loadDataBtn').addEventListener('click', loadData);
|
||
document.getElementById('copyAllBtn').addEventListener('click', copyAllToClipboard);
|
||
document.getElementById('exportBtn').addEventListener('click', exportToIntect);
|
||
|
||
// Load initial data
|
||
loadData();
|
||
</script>
|
||
</body>
|
||
</html>
|