1783 lines
60 KiB
HTML
1783 lines
60 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="da">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Arbejdstidsplan</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');
|
||
|
||
@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;
|
||
}
|
||
|
||
:root {
|
||
--color-background: #ffffff;
|
||
--color-background-alt: #f8f9fa;
|
||
--color-border: #e5e7eb;
|
||
--color-text: #1f2937;
|
||
--color-text-secondary: #6b7280;
|
||
--color-text-muted: #9ca3af;
|
||
--color-teal: #00897b;
|
||
--color-teal-light: #ccfbf1;
|
||
--font-mono: 'JetBrains Mono', monospace;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Poppins', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
background: var(--color-background-alt);
|
||
color: var(--color-text);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* ==========================================
|
||
PAGE LAYOUT
|
||
========================================== */
|
||
swp-page {
|
||
display: block;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 24px;
|
||
}
|
||
|
||
swp-page-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
swp-page-title {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-page-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* ==========================================
|
||
WEEK NAVIGATION
|
||
========================================== */
|
||
swp-week-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
swp-week-date-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
swp-week-nav button {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
background: var(--color-background);
|
||
cursor: pointer;
|
||
color: var(--color-text);
|
||
font-size: 16px;
|
||
}
|
||
|
||
swp-week-nav button:hover {
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
swp-week-label {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--color-text-secondary);
|
||
min-width: 200px;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ==========================================
|
||
BUTTONS
|
||
========================================== */
|
||
swp-button {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 16px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
border: none;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
swp-button.primary {
|
||
background: var(--color-teal);
|
||
color: white;
|
||
}
|
||
|
||
swp-button.primary:hover {
|
||
background: #0f766e;
|
||
}
|
||
|
||
swp-button.secondary {
|
||
background: var(--color-background);
|
||
color: var(--color-text);
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
swp-button.secondary:hover {
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
/* ==========================================
|
||
SCHEDULE TABLE (flat grid with cell borders)
|
||
========================================== */
|
||
swp-schedule-table {
|
||
display: grid;
|
||
grid-template-columns: 180px repeat(7, minmax(100px, 1fr));
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--color-border);
|
||
}
|
||
|
||
swp-schedule-cell {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
padding: 12px 16px;
|
||
min-height: 60px;
|
||
background: var(--color-background);
|
||
border-right: 1px solid var(--color-border);
|
||
border-bottom: 1px solid var(--color-border);
|
||
user-select: none;
|
||
}
|
||
|
||
/* Sidste kolonne: ingen højre border */
|
||
swp-schedule-cell:nth-child(8n) {
|
||
border-right: none;
|
||
}
|
||
|
||
/* Sidste række: ingen bund border */
|
||
swp-schedule-cell:nth-last-child(-n+8) {
|
||
border-bottom: none;
|
||
}
|
||
|
||
swp-schedule-cell.header {
|
||
background: var(--color-background-alt);
|
||
font-weight: 500;
|
||
font-size: 13px;
|
||
color: var(--color-text-secondary);
|
||
min-height: 48px;
|
||
text-align: center;
|
||
align-items: center;
|
||
}
|
||
|
||
swp-schedule-cell.header.week-number {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
/* Lukkedag styling */
|
||
body.edit-mode swp-schedule-cell.header:not(.week-number) {
|
||
cursor: pointer;
|
||
}
|
||
|
||
body.edit-mode swp-schedule-cell.header:not(.week-number):hover {
|
||
background: var(--color-border);
|
||
}
|
||
|
||
swp-schedule-cell.header.closed {
|
||
background: color-mix(in srgb, #f59e0b 10%, var(--color-background-alt));
|
||
border-top: 2px solid #f59e0b !important;
|
||
border-left: 2px solid #f59e0b !important;
|
||
border-right: 2px solid #f59e0b !important;
|
||
border-bottom: none !important;
|
||
}
|
||
|
||
swp-schedule-cell.header.closed swp-day-name {
|
||
color: #d97706;
|
||
}
|
||
|
||
/* Celler i lukket kolonne */
|
||
swp-schedule-cell.day.closed-day {
|
||
background: color-mix(in srgb, #f59e0b 6%, var(--color-background));
|
||
border-left: 2px solid #f59e0b !important;
|
||
border-right: 2px solid #f59e0b !important;
|
||
}
|
||
|
||
/* Sidste celle i lukket kolonne får bund-border */
|
||
swp-schedule-cell.day.closed-day:nth-last-child(-n+8) {
|
||
border-bottom: 2px solid #f59e0b !important;
|
||
}
|
||
|
||
swp-schedule-cell.day.closed-day swp-time-display {
|
||
opacity: 0.5;
|
||
}
|
||
|
||
swp-schedule-cell.employee {
|
||
align-items: flex-start;
|
||
gap: 2px;
|
||
}
|
||
|
||
swp-schedule-cell.day {
|
||
align-items: center;
|
||
text-align: center;
|
||
position: relative;
|
||
}
|
||
|
||
/* ==========================================
|
||
EMPLOYEE INFO
|
||
========================================== */
|
||
swp-employee-name {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-employee-hours {
|
||
font-family: var(--font-mono);
|
||
font-size: 12px;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
/* ==========================================
|
||
TIME DISPLAY
|
||
========================================== */
|
||
swp-time-display {
|
||
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-display.off {
|
||
background: transparent;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
swp-time-display.off.off-override {
|
||
background: color-mix(in srgb, #7c3aed 12%, white);
|
||
color: #6d28d9;
|
||
}
|
||
|
||
swp-time-display.vacation {
|
||
background: color-mix(in srgb, #f59e0b 15%, white);
|
||
color: #b45309;
|
||
}
|
||
|
||
swp-time-display.sick {
|
||
background: color-mix(in srgb, #ef4444 15%, white);
|
||
color: #dc2626;
|
||
}
|
||
|
||
/* ==========================================
|
||
DAY HEADER
|
||
========================================== */
|
||
swp-day-name {
|
||
font-weight: 500;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-day-date {
|
||
font-size: 12px;
|
||
color: var(--color-text-muted);
|
||
font-weight: 400;
|
||
}
|
||
|
||
/* ==========================================
|
||
EDIT MODE
|
||
========================================== */
|
||
body.edit-mode swp-schedule-cell.day {
|
||
cursor: pointer;
|
||
position: relative;
|
||
}
|
||
|
||
body.edit-mode swp-schedule-cell.day:hover {
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
|
||
body.edit-mode swp-schedule-cell.day.selected {
|
||
background: color-mix(in srgb, var(--color-teal) 12%, white);
|
||
border-color: var(--color-teal);
|
||
border-width: 2px;
|
||
border-left: 2px solid var(--color-teal);
|
||
border-top: 2px solid var(--color-teal);
|
||
/* Kompenser for ekstra border-bredde */
|
||
margin-left: -1px;
|
||
margin-top: -1px;
|
||
padding: 11px 15px;
|
||
}
|
||
|
||
/* Én border mellem tilstødende valgte celler */
|
||
/* Venstre celle beholder sin højre border, højre celle fjerner sin venstre */
|
||
body.edit-mode swp-schedule-cell.day.selected.adj-left {
|
||
border-left: none;
|
||
margin-left: 0;
|
||
padding-left: 12px;
|
||
}
|
||
/* Øverste celle beholder sin bund border, nederste celle fjerner sin top */
|
||
body.edit-mode swp-schedule-cell.day.selected.adj-top {
|
||
border-top: none;
|
||
margin-top: 0;
|
||
padding-top: 12px;
|
||
}
|
||
|
||
/* ==========================================
|
||
DRAWER
|
||
========================================== */
|
||
swp-drawer {
|
||
display: block;
|
||
position: fixed;
|
||
top: 0;
|
||
right: -400px;
|
||
width: 400px;
|
||
height: 100vh;
|
||
background: var(--color-background);
|
||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
|
||
z-index: 101;
|
||
transition: right 0.3s ease;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
swp-drawer.open {
|
||
right: 0;
|
||
}
|
||
|
||
swp-drawer-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--color-border);
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
swp-drawer-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-drawer-subtitle {
|
||
font-size: 13px;
|
||
color: var(--color-text-secondary);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
swp-drawer-close {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 32px;
|
||
height: 32px;
|
||
border: none;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
border-radius: 6px;
|
||
color: var(--color-text-secondary);
|
||
font-size: 20px;
|
||
}
|
||
|
||
swp-drawer-close:hover {
|
||
background: var(--color-border);
|
||
}
|
||
|
||
swp-drawer-content {
|
||
display: block;
|
||
padding: 24px;
|
||
}
|
||
|
||
swp-drawer-section {
|
||
display: block;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
swp-drawer-section-title {
|
||
display: block;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
color: var(--color-text-secondary);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
/* ==========================================
|
||
DRAWER FORM ELEMENTS
|
||
========================================== */
|
||
swp-form-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
swp-form-label {
|
||
font-size: 11px;
|
||
font-weight: 400;
|
||
color: var(--color-text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
swp-form-label .optional,
|
||
swp-form-label .auto {
|
||
font-weight: 400;
|
||
text-transform: none;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
swp-form-value {
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
color: var(--color-text);
|
||
}
|
||
|
||
swp-form-divider {
|
||
display: block;
|
||
height: 1px;
|
||
background: var(--color-border);
|
||
margin: 20px 0;
|
||
}
|
||
|
||
swp-form-group {
|
||
display: block;
|
||
padding: 16px;
|
||
background: var(--color-background-alt);
|
||
border-radius: 8px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
swp-form-group swp-form-row:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
swp-form-hint {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: var(--color-text-muted);
|
||
margin: -8px 0 16px 0;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
swp-form-select {
|
||
display: block;
|
||
}
|
||
|
||
swp-form-select select {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: var(--color-text);
|
||
background: var(--color-background);
|
||
cursor: pointer;
|
||
}
|
||
|
||
swp-form-select select:focus {
|
||
outline: none;
|
||
border-color: var(--color-teal);
|
||
}
|
||
|
||
swp-drawer-content input[type="time"],
|
||
swp-drawer-content input[type="text"],
|
||
swp-drawer-content input[type="date"] {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
color: var(--color-text);
|
||
background: var(--color-background);
|
||
}
|
||
|
||
swp-drawer-content input[type="time"] {
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
swp-drawer-content input:focus {
|
||
outline: none;
|
||
border-color: var(--color-teal);
|
||
}
|
||
|
||
swp-drawer-content input::placeholder {
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
/* ==========================================
|
||
TOGGLE OPTIONS (Enkelt/Gentagelse)
|
||
========================================== */
|
||
swp-toggle-options {
|
||
display: flex;
|
||
gap: 0;
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 6px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
swp-toggle-option {
|
||
flex: 1;
|
||
padding: 10px 16px;
|
||
text-align: center;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
background: var(--color-background);
|
||
border-right: 1px solid var(--color-border);
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
swp-toggle-option:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
swp-toggle-option:hover {
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
swp-toggle-option.selected {
|
||
background: var(--color-teal);
|
||
color: white;
|
||
}
|
||
|
||
/* ==========================================
|
||
STATUS OPTIONS (badge style like poc-detail-drawer)
|
||
========================================== */
|
||
swp-status-options {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
swp-status-option {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
background: var(--color-background-alt);
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
swp-status-option::before {
|
||
content: '';
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
swp-status-option[data-status="work"] {
|
||
--status-color: var(--color-teal);
|
||
}
|
||
swp-status-option[data-status="off"] {
|
||
--status-color: #7c3aed;
|
||
}
|
||
swp-status-option[data-status="vacation"] {
|
||
--status-color: #f59e0b;
|
||
}
|
||
swp-status-option[data-status="sick"] {
|
||
--status-color: #e53935;
|
||
}
|
||
|
||
swp-status-option::before {
|
||
background: var(--status-color);
|
||
}
|
||
|
||
swp-status-option:hover {
|
||
background: var(--color-border);
|
||
}
|
||
|
||
swp-status-option.selected {
|
||
background: color-mix(in srgb, var(--status-color) 15%, white);
|
||
color: var(--status-color);
|
||
}
|
||
|
||
/* ==========================================
|
||
DRAWER FOOTER
|
||
========================================== */
|
||
swp-drawer-footer {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 20px 24px;
|
||
border-top: 1px solid var(--color-border);
|
||
background: var(--color-background-alt);
|
||
}
|
||
|
||
swp-drawer-footer swp-button {
|
||
flex: 1;
|
||
}
|
||
|
||
/* ==========================================
|
||
OVERRIDE DISPLAY
|
||
========================================== */
|
||
swp-time-override {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 2px;
|
||
}
|
||
|
||
swp-time-original {
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
color: var(--color-text-muted);
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
swp-override-badge {
|
||
position: absolute;
|
||
top: 4px;
|
||
right: 4px;
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #f59e0b;
|
||
}
|
||
|
||
/* ==========================================
|
||
TIME RANGE SLIDER
|
||
========================================== */
|
||
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-fill:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
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-slider input[type="range"]::-moz-range-thumb {
|
||
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 {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 2px;
|
||
min-width: 100px;
|
||
text-align: center;
|
||
background: var(--color-background-alt);
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
swp-time-range-times {
|
||
font-size: 13px;
|
||
font-family: var(--font-mono);
|
||
font-weight: 500;
|
||
color: var(--color-text);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
swp-time-range-duration {
|
||
font-size: 11px;
|
||
font-family: var(--font-mono);
|
||
color: var(--color-text-secondary);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* ==========================================
|
||
DRAWER EMPLOYEE DISPLAY
|
||
========================================== */
|
||
swp-employee-display {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
swp-employee-display swp-employee-avatar {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, var(--color-teal) 0%, #00695c 100%);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 600;
|
||
font-size: 12px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
swp-employee-display.empty swp-employee-avatar {
|
||
background: var(--color-border);
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
swp-employee-display.multi swp-employee-avatar {
|
||
background: var(--color-text-muted);
|
||
}
|
||
|
||
/* Note icon i celle */
|
||
swp-note-icon {
|
||
position: absolute;
|
||
bottom: 7px;
|
||
right: 4px;
|
||
width: 14px;
|
||
height: 14px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
swp-note-icon img {
|
||
width: 100%;
|
||
height: 100%;
|
||
filter: brightness(0) saturate(100%) invert(20%) sepia(30%) saturate(700%) hue-rotate(190deg) brightness(90%) contrast(95%);
|
||
}
|
||
|
||
/* Override indikator - viser original tid overstreget */
|
||
swp-override-original {
|
||
position: absolute;
|
||
bottom: 1px;
|
||
left: 8px;
|
||
font-family: var(--font-mono);
|
||
font-size: 10px;
|
||
color: var(--color-text-muted);
|
||
text-decoration: line-through;
|
||
opacity: 0.7;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<swp-page>
|
||
<swp-page-header>
|
||
<swp-page-title>Arbejdstidsplan</swp-page-title>
|
||
<swp-page-actions>
|
||
<swp-button class="primary" id="editModeBtn">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||
</svg>
|
||
Rediger
|
||
</swp-button>
|
||
</swp-page-actions>
|
||
</swp-page-header>
|
||
|
||
<swp-week-nav>
|
||
<swp-week-date-nav>
|
||
<button id="prevWeek">‹</button>
|
||
<swp-week-label>23. - 29. december 2025</swp-week-label>
|
||
<button id="nextWeek">›</button>
|
||
</swp-week-date-nav>
|
||
</swp-week-nav>
|
||
|
||
<swp-schedule-table>
|
||
<!-- Header row -->
|
||
<swp-schedule-cell class="header week-number">Uge 52</swp-schedule-cell>
|
||
<swp-schedule-cell class="header">
|
||
<swp-day-name>Mandag</swp-day-name>
|
||
<swp-day-date>23/12</swp-day-date>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="header">
|
||
<swp-day-name>Tirsdag</swp-day-name>
|
||
<swp-day-date>24/12</swp-day-date>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="header closed">
|
||
<swp-day-name>Onsdag</swp-day-name>
|
||
<swp-day-date>25/12</swp-day-date>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="header">
|
||
<swp-day-name>Torsdag</swp-day-name>
|
||
<swp-day-date>26/12</swp-day-date>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="header">
|
||
<swp-day-name>Fredag</swp-day-name>
|
||
<swp-day-date>27/12</swp-day-date>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="header">
|
||
<swp-day-name>Lørdag</swp-day-name>
|
||
<swp-day-date>28/12</swp-day-date>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="header">
|
||
<swp-day-name>Søndag</swp-day-name>
|
||
<swp-day-date>29/12</swp-day-date>
|
||
</swp-schedule-cell>
|
||
|
||
<!-- Anna Sørensen -->
|
||
<swp-schedule-cell class="employee">
|
||
<swp-employee-name>Anna Sørensen</swp-employee-name>
|
||
<swp-employee-hours>32 timer</swp-employee-hours>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Anna Sørensen" data-day="Man" data-date="23/12"><swp-time-display>09:00 - 17:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Anna Sørensen" data-day="Tir" data-date="24/12"><swp-time-display>09:00 - 13:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day closed-day" data-employee="Anna Sørensen" data-day="Ons" data-date="25/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Anna Sørensen" data-day="Tor" data-date="26/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Anna Sørensen" data-day="Fre" data-date="27/12"><swp-time-display>09:00 - 17:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Anna Sørensen" data-day="Lør" data-date="28/12"><swp-time-display>10:00 - 14:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Anna Sørensen" data-day="Søn" data-date="29/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
|
||
<!-- Mette Jensen -->
|
||
<swp-schedule-cell class="employee">
|
||
<swp-employee-name>Mette Jensen</swp-employee-name>
|
||
<swp-employee-hours>40 timer</swp-employee-hours>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Mette Jensen" data-day="Man" data-date="23/12"><swp-time-display>10:00 - 18:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Mette Jensen" data-day="Tir" data-date="24/12"><swp-time-display>10:00 - 18:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day closed-day" data-employee="Mette Jensen" data-day="Ons" data-date="25/12"><swp-time-display class="vacation">Ferie</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Mette Jensen" data-day="Tor" data-date="26/12"><swp-time-display class="vacation">Ferie</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Mette Jensen" data-day="Fre" data-date="27/12"><swp-time-display class="vacation">Ferie</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Mette Jensen" data-day="Lør" data-date="28/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Mette Jensen" data-day="Søn" data-date="29/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
|
||
<!-- Louise Nielsen -->
|
||
<swp-schedule-cell class="employee">
|
||
<swp-employee-name>Louise Nielsen</swp-employee-name>
|
||
<swp-employee-hours>37 timer</swp-employee-hours>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Louise Nielsen" data-day="Man" data-date="23/12"><swp-time-display>09:00 - 17:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Louise Nielsen" data-day="Tir" data-date="24/12"><swp-time-display>09:00 - 17:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day closed-day" data-employee="Louise Nielsen" data-day="Ons" data-date="25/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Louise Nielsen" data-day="Tor" data-date="26/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Louise Nielsen" data-day="Fre" data-date="27/12"><swp-time-display>09:00 - 17:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Louise Nielsen" data-day="Lør" data-date="28/12"><swp-time-display>09:00 - 14:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Louise Nielsen" data-day="Søn" data-date="29/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
|
||
<!-- Katrine Pedersen -->
|
||
<swp-schedule-cell class="employee">
|
||
<swp-employee-name>Katrine Pedersen</swp-employee-name>
|
||
<swp-employee-hours>24 timer</swp-employee-hours>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Katrine Pedersen" data-day="Man" data-date="23/12"><swp-time-display>12:00 - 20:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Katrine Pedersen" data-day="Tir" data-date="24/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day closed-day" data-employee="Katrine Pedersen" data-day="Ons" data-date="25/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Katrine Pedersen" data-day="Tor" data-date="26/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Katrine Pedersen" data-day="Fre" data-date="27/12"><swp-time-display>12:00 - 20:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Katrine Pedersen" data-day="Lør" data-date="28/12"><swp-time-display>10:00 - 18:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Katrine Pedersen" data-day="Søn" data-date="29/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
|
||
<!-- Sofie Andersen -->
|
||
<swp-schedule-cell class="employee">
|
||
<swp-employee-name>Sofie Andersen</swp-employee-name>
|
||
<swp-employee-hours>20 timer</swp-employee-hours>
|
||
</swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Sofie Andersen" data-day="Man" data-date="23/12"><swp-time-display class="sick">Syg</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Sofie Andersen" data-day="Tir" data-date="24/12"><swp-time-display>09:00 - 15:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day closed-day" data-employee="Sofie Andersen" data-day="Ons" data-date="25/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Sofie Andersen" data-day="Tor" data-date="26/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Sofie Andersen" data-day="Fre" data-date="27/12"><swp-time-display>09:00 - 15:00</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Sofie Andersen" data-day="Lør" data-date="28/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
<swp-schedule-cell class="day" data-employee="Sofie Andersen" data-day="Søn" data-date="29/12"><swp-time-display class="off">—</swp-time-display></swp-schedule-cell>
|
||
</swp-schedule-table>
|
||
</swp-page>
|
||
|
||
<!-- DRAWER -->
|
||
<swp-drawer id="scheduleDrawer">
|
||
<swp-drawer-header>
|
||
<swp-drawer-title id="drawerTitle">Redigér vagt</swp-drawer-title>
|
||
<swp-drawer-close id="drawerClose">×</swp-drawer-close>
|
||
</swp-drawer-header>
|
||
|
||
<swp-drawer-content>
|
||
<swp-form-row>
|
||
<swp-form-label>Medarbejder</swp-form-label>
|
||
<swp-employee-display id="fieldEmployeeDisplay">
|
||
<swp-employee-avatar id="fieldAvatar"></swp-employee-avatar>
|
||
<swp-form-value id="fieldEmployee">Vælg celle...</swp-form-value>
|
||
</swp-employee-display>
|
||
</swp-form-row>
|
||
|
||
<swp-form-row>
|
||
<swp-form-label>Dato</swp-form-label>
|
||
<swp-form-value id="fieldDate">—</swp-form-value>
|
||
</swp-form-row>
|
||
|
||
<swp-form-divider></swp-form-divider>
|
||
|
||
<swp-form-row>
|
||
<swp-form-label>Status</swp-form-label>
|
||
<swp-status-options id="statusOptions">
|
||
<swp-status-option data-status="work" class="selected">Arbejde</swp-status-option>
|
||
<swp-status-option data-status="off">Fri</swp-status-option>
|
||
<swp-status-option data-status="vacation">Ferie</swp-status-option>
|
||
<swp-status-option data-status="sick">Syg</swp-status-option>
|
||
</swp-status-options>
|
||
</swp-form-row>
|
||
|
||
<swp-form-row id="timeRow">
|
||
<swp-form-label>Tidsrum</swp-form-label>
|
||
<swp-time-range id="drawerTimeRange">
|
||
<swp-time-range-slider>
|
||
<swp-time-range-track></swp-time-range-track>
|
||
<swp-time-range-fill></swp-time-range-fill>
|
||
<input type="range" class="range-start" min="0" max="60" value="12" step="1">
|
||
<input type="range" class="range-end" min="0" max="60" value="44" step="1">
|
||
</swp-time-range-slider>
|
||
<swp-time-range-label>
|
||
<swp-time-range-times>09:00 – 17:00</swp-time-range-times>
|
||
<swp-time-range-duration>8 timer</swp-time-range-duration>
|
||
</swp-time-range-label>
|
||
</swp-time-range>
|
||
</swp-form-row>
|
||
|
||
<swp-form-row>
|
||
<swp-form-label>Note <span class="optional">(valgfrit)</span></swp-form-label>
|
||
<input type="text" id="fieldNote" placeholder="F.eks. Aftenvagt">
|
||
</swp-form-row>
|
||
|
||
<swp-form-divider></swp-form-divider>
|
||
|
||
<swp-form-row>
|
||
<swp-form-label>Type</swp-form-label>
|
||
<swp-toggle-options id="typeOptions">
|
||
<swp-toggle-option data-value="single">Enkelt</swp-toggle-option>
|
||
<swp-toggle-option data-value="template" class="selected">Gentagelse</swp-toggle-option>
|
||
</swp-toggle-options>
|
||
</swp-form-row>
|
||
|
||
<swp-form-group id="repeatGroup">
|
||
<swp-form-row>
|
||
<swp-form-label>Gentag</swp-form-label>
|
||
<swp-form-select>
|
||
<select id="repeatInterval">
|
||
<option value="1">Hver uge</option>
|
||
<option value="2">Hver 2. uge</option>
|
||
<option value="3">Hver 3. uge</option>
|
||
<option value="4">Hver 4. uge</option>
|
||
</select>
|
||
</swp-form-select>
|
||
</swp-form-row>
|
||
|
||
<swp-form-hint>
|
||
Gentagelser bruger valgt dato som startuge.
|
||
</swp-form-hint>
|
||
|
||
<swp-form-row>
|
||
<swp-form-label>Slutdato <span class="optional">(valgfrit)</span></swp-form-label>
|
||
<input type="date" id="repeatEndDate">
|
||
</swp-form-row>
|
||
|
||
<swp-form-row>
|
||
<swp-form-label>Ugedag <span class="auto">(auto)</span></swp-form-label>
|
||
<swp-form-value id="fieldWeekday">—</swp-form-value>
|
||
</swp-form-row>
|
||
</swp-form-group>
|
||
</swp-drawer-content>
|
||
|
||
<swp-drawer-footer>
|
||
<swp-button class="secondary" id="drawerCancel">Annuller</swp-button>
|
||
<swp-button class="primary" id="drawerSave">Gem</swp-button>
|
||
</swp-drawer-footer>
|
||
</swp-drawer>
|
||
|
||
<script>
|
||
// ==========================================
|
||
// EDIT MODE
|
||
// ==========================================
|
||
const editModeBtn = document.getElementById('editModeBtn');
|
||
let isEditMode = false;
|
||
|
||
editModeBtn.addEventListener('click', () => {
|
||
isEditMode = !isEditMode;
|
||
document.body.classList.toggle('edit-mode', isEditMode);
|
||
|
||
if (isEditMode) {
|
||
editModeBtn.innerHTML = `
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||
<polyline points="7 3 7 8 15 8"/>
|
||
</svg>
|
||
Færdig`;
|
||
openDrawer();
|
||
showEmptyState();
|
||
} else {
|
||
editModeBtn.innerHTML = `
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||
</svg>
|
||
Rediger`;
|
||
closeDrawer();
|
||
clearSelection();
|
||
}
|
||
});
|
||
|
||
// ==========================================
|
||
// DRAWER
|
||
// ==========================================
|
||
const drawer = document.getElementById('scheduleDrawer');
|
||
const drawerClose = document.getElementById('drawerClose');
|
||
const drawerCancel = document.getElementById('drawerCancel');
|
||
const drawerSave = document.getElementById('drawerSave');
|
||
const drawerContent = document.querySelector('swp-drawer-content');
|
||
const drawerFooter = document.querySelector('swp-drawer-footer');
|
||
|
||
// Form fields
|
||
const fieldEmployee = document.getElementById('fieldEmployee');
|
||
const fieldEmployeeDisplay = document.getElementById('fieldEmployeeDisplay');
|
||
const fieldAvatar = document.getElementById('fieldAvatar');
|
||
const fieldDate = document.getElementById('fieldDate');
|
||
const fieldWeekday = document.getElementById('fieldWeekday');
|
||
const timeRow = document.getElementById('timeRow');
|
||
const repeatGroup = document.getElementById('repeatGroup');
|
||
const drawerTimeRange = document.getElementById('drawerTimeRange');
|
||
|
||
function getInitials(name) {
|
||
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||
}
|
||
|
||
const weekdays = ['Søndag', 'Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag'];
|
||
|
||
// ==========================================
|
||
// TIME RANGE SLIDER
|
||
// ==========================================
|
||
const TIME_RANGE_MAX = 60; // 15 hours (06:00-21:00) * 4 intervals
|
||
|
||
function valueToTime(value) {
|
||
const totalMinutes = (value * 15) + (6 * 60); // Add 6 hour offset
|
||
const hours = Math.floor(totalMinutes / 60);
|
||
const minutes = totalMinutes % 60;
|
||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
function timeToValue(timeStr) {
|
||
const [hours, minutes] = timeStr.split(':').map(Number);
|
||
const totalMinutes = hours * 60 + minutes;
|
||
return Math.round((totalMinutes - 6 * 60) / 15);
|
||
}
|
||
|
||
function updateTimeRange(slider) {
|
||
const startInput = slider.querySelector('.range-start');
|
||
const endInput = slider.querySelector('.range-end');
|
||
const fill = slider.querySelector('swp-time-range-fill');
|
||
const labelContainer = slider.closest('swp-time-range').querySelector('swp-time-range-label');
|
||
const timesEl = labelContainer.querySelector('swp-time-range-times');
|
||
const durationEl = labelContainer.querySelector('swp-time-range-duration');
|
||
|
||
let startVal = parseInt(startInput.value);
|
||
let endVal = parseInt(endInput.value);
|
||
|
||
// Ensure start doesn't exceed end
|
||
if (startVal > endVal) {
|
||
if (startInput === document.activeElement) {
|
||
startInput.value = endVal;
|
||
startVal = endVal;
|
||
} else {
|
||
endInput.value = startVal;
|
||
endVal = startVal;
|
||
}
|
||
}
|
||
|
||
// Update fill bar position
|
||
const startPercent = (startVal / TIME_RANGE_MAX) * 100;
|
||
const endPercent = (endVal / TIME_RANGE_MAX) * 100;
|
||
fill.style.left = startPercent + '%';
|
||
fill.style.width = (endPercent - startPercent) + '%';
|
||
|
||
// Calculate duration in hours
|
||
const durationIntervals = endVal - startVal;
|
||
const durationMinutes = durationIntervals * 15;
|
||
const durationHours = durationMinutes / 60;
|
||
const durationText = durationHours % 1 === 0
|
||
? `${durationHours} timer`
|
||
: `${durationHours.toFixed(1).replace('.', ',')} timer`;
|
||
|
||
// Update time range and duration separately
|
||
timesEl.textContent = `${valueToTime(startVal)} – ${valueToTime(endVal)}`;
|
||
durationEl.textContent = durationText;
|
||
}
|
||
|
||
function initTimeRangeSlider(sliderContainer) {
|
||
const slider = sliderContainer.querySelector('swp-time-range-slider');
|
||
const startInput = slider.querySelector('.range-start');
|
||
const endInput = slider.querySelector('.range-end');
|
||
const fill = slider.querySelector('swp-time-range-fill');
|
||
const track = slider.querySelector('swp-time-range-track');
|
||
|
||
// Initialize
|
||
updateTimeRange(slider);
|
||
|
||
startInput.addEventListener('input', () => {
|
||
updateTimeRange(slider);
|
||
updateSelectedCellsTime();
|
||
});
|
||
endInput.addEventListener('input', () => {
|
||
updateTimeRange(slider);
|
||
updateSelectedCellsTime();
|
||
});
|
||
|
||
// Drag fill bar to move entire range
|
||
let isDragging = false;
|
||
let dragStartX = 0;
|
||
let dragStartValues = { start: 0, end: 0 };
|
||
|
||
fill.addEventListener('mousedown', (e) => {
|
||
isDragging = true;
|
||
dragStartX = e.clientX;
|
||
dragStartValues.start = parseInt(startInput.value);
|
||
dragStartValues.end = parseInt(endInput.value);
|
||
e.preventDefault();
|
||
});
|
||
|
||
document.addEventListener('mousemove', (e) => {
|
||
if (!isDragging) return;
|
||
|
||
const sliderWidth = track.offsetWidth;
|
||
const deltaX = e.clientX - dragStartX;
|
||
const deltaValue = Math.round((deltaX / sliderWidth) * TIME_RANGE_MAX);
|
||
|
||
const duration = dragStartValues.end - dragStartValues.start;
|
||
let newStart = dragStartValues.start + deltaValue;
|
||
let newEnd = dragStartValues.end + deltaValue;
|
||
|
||
// Clamp to bounds
|
||
if (newStart < 0) {
|
||
newStart = 0;
|
||
newEnd = duration;
|
||
}
|
||
if (newEnd > TIME_RANGE_MAX) {
|
||
newEnd = TIME_RANGE_MAX;
|
||
newStart = TIME_RANGE_MAX - duration;
|
||
}
|
||
|
||
startInput.value = newStart;
|
||
endInput.value = newEnd;
|
||
updateTimeRange(slider);
|
||
updateSelectedCellsTime();
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
isDragging = false;
|
||
});
|
||
}
|
||
|
||
// Initialize the drawer time range slider
|
||
initTimeRangeSlider(drawerTimeRange);
|
||
|
||
// Opdater valgte celler i realtid når tiden ændres
|
||
function updateSelectedCellsTime() {
|
||
const selectedStatus = document.querySelector('#statusOptions swp-status-option.selected');
|
||
const status = selectedStatus ? selectedStatus.dataset.status : 'work';
|
||
|
||
// Kun opdater hvis status er "work"
|
||
if (status !== 'work') return;
|
||
|
||
const slider = drawerTimeRange.querySelector('swp-time-range-slider');
|
||
const startVal = parseInt(slider.querySelector('.range-start').value);
|
||
const endVal = parseInt(slider.querySelector('.range-end').value);
|
||
const startTime = valueToTime(startVal);
|
||
const endTime = valueToTime(endVal);
|
||
const formattedTime = `${startTime} - ${endTime}`;
|
||
|
||
selectedCells.forEach(cell => {
|
||
const timeDisplay = cell.querySelector('swp-time-display');
|
||
if (timeDisplay && !timeDisplay.classList.contains('off') &&
|
||
!timeDisplay.classList.contains('vacation') &&
|
||
!timeDisplay.classList.contains('sick')) {
|
||
timeDisplay.textContent = formattedTime;
|
||
}
|
||
});
|
||
}
|
||
|
||
let selectedCells = [];
|
||
const GRID_COLUMNS = 8; // 1 employee + 7 days
|
||
const allCells = Array.from(document.querySelectorAll('swp-schedule-table > swp-schedule-cell'));
|
||
|
||
function updateAdjacentClasses() {
|
||
// Fjern adjacency klasser først
|
||
selectedCells.forEach(c => c.classList.remove('adj-left', 'adj-right', 'adj-top', 'adj-bottom'));
|
||
|
||
// Find indices af alle valgte celler
|
||
const selectedIndices = selectedCells.map(cell => allCells.indexOf(cell));
|
||
|
||
selectedCells.forEach(cell => {
|
||
const idx = allCells.indexOf(cell);
|
||
const col = idx % GRID_COLUMNS;
|
||
|
||
// Venstre nabo
|
||
if (col > 1 && selectedIndices.includes(idx - 1)) {
|
||
cell.classList.add('adj-left');
|
||
}
|
||
// Højre nabo
|
||
if (col < GRID_COLUMNS - 1 && selectedIndices.includes(idx + 1)) {
|
||
cell.classList.add('adj-right');
|
||
}
|
||
// Nabo ovenover
|
||
if (selectedIndices.includes(idx - GRID_COLUMNS)) {
|
||
cell.classList.add('adj-top');
|
||
}
|
||
// Nabo nedenunder
|
||
if (selectedIndices.includes(idx + GRID_COLUMNS)) {
|
||
cell.classList.add('adj-bottom');
|
||
}
|
||
});
|
||
}
|
||
|
||
function showEmptyState() {
|
||
fieldEmployee.textContent = 'Vælg celle...';
|
||
fieldAvatar.textContent = '?';
|
||
fieldEmployeeDisplay.classList.add('empty');
|
||
fieldEmployeeDisplay.classList.remove('multi');
|
||
fieldDate.textContent = '—';
|
||
fieldWeekday.textContent = '—';
|
||
drawerContent.style.opacity = '0.5';
|
||
drawerContent.style.pointerEvents = 'none';
|
||
drawerFooter.style.display = 'none';
|
||
}
|
||
|
||
function showEditState() {
|
||
drawerContent.style.opacity = '1';
|
||
drawerContent.style.pointerEvents = 'auto';
|
||
drawerFooter.style.display = 'flex';
|
||
}
|
||
|
||
function updateDrawerFields() {
|
||
if (selectedCells.length === 0) {
|
||
showEmptyState();
|
||
return;
|
||
}
|
||
|
||
showEditState();
|
||
fieldEmployeeDisplay.classList.remove('empty', 'multi');
|
||
|
||
if (selectedCells.length === 1) {
|
||
const cell = selectedCells[0];
|
||
const employeeName = cell.dataset.employee;
|
||
fieldEmployee.textContent = employeeName;
|
||
fieldAvatar.textContent = getInitials(employeeName);
|
||
fieldDate.textContent = cell.dataset.date + '/2025';
|
||
|
||
// Calculate weekday from day name
|
||
const dayMap = { 'Man': 1, 'Tir': 2, 'Ons': 3, 'Tor': 4, 'Fre': 5, 'Lør': 6, 'Søn': 0 };
|
||
fieldWeekday.textContent = weekdays[dayMap[cell.dataset.day]];
|
||
|
||
// Pre-fill form with current cell values
|
||
prefillFormFromCell(cell);
|
||
} else {
|
||
const employees = [...new Set(selectedCells.map(c => c.dataset.employee))];
|
||
const days = [...new Set(selectedCells.map(c => c.dataset.day))];
|
||
|
||
if (employees.length === 1) {
|
||
fieldEmployee.textContent = employees[0];
|
||
fieldAvatar.textContent = getInitials(employees[0]);
|
||
fieldDate.textContent = `${selectedCells.length} dage valgt`;
|
||
} else if (days.length === 1) {
|
||
fieldEmployee.textContent = `${selectedCells.length} medarbejdere`;
|
||
fieldAvatar.textContent = employees.length;
|
||
fieldEmployeeDisplay.classList.add('multi');
|
||
fieldDate.textContent = selectedCells[0].dataset.date + '/2025';
|
||
} else {
|
||
fieldEmployee.textContent = `${selectedCells.length} valgt`;
|
||
fieldAvatar.textContent = selectedCells.length;
|
||
fieldEmployeeDisplay.classList.add('multi');
|
||
fieldDate.textContent = `${employees.length} medarbejdere, ${days.length} dage`;
|
||
}
|
||
fieldWeekday.textContent = days.length === 1 ? weekdays[{ 'Man': 1, 'Tir': 2, 'Ons': 3, 'Tor': 4, 'Fre': 5, 'Lør': 6, 'Søn': 0 }[days[0]]] : 'Flere dage';
|
||
|
||
// Reset form to default for multi-select
|
||
resetFormToDefault();
|
||
}
|
||
}
|
||
|
||
function prefillFormFromCell(cell) {
|
||
const timeDisplay = cell.querySelector('swp-time-display');
|
||
if (!timeDisplay) return;
|
||
|
||
// Determine current status
|
||
let status = 'work';
|
||
if (timeDisplay.classList.contains('off')) status = 'off';
|
||
else if (timeDisplay.classList.contains('vacation')) status = 'vacation';
|
||
else if (timeDisplay.classList.contains('sick')) status = 'sick';
|
||
|
||
// Update status options
|
||
document.querySelectorAll('#statusOptions swp-status-option').forEach(opt => {
|
||
opt.classList.toggle('selected', opt.dataset.status === status);
|
||
});
|
||
|
||
// Show/hide time slider based on status
|
||
const showTime = status === 'work';
|
||
timeRow.style.display = showTime ? 'flex' : 'none';
|
||
|
||
// Parse and fill time if work status
|
||
if (status === 'work') {
|
||
const timeText = timeDisplay.textContent.trim();
|
||
const timeMatch = timeText.match(/(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
||
if (timeMatch) {
|
||
const slider = drawerTimeRange.querySelector('swp-time-range-slider');
|
||
slider.querySelector('.range-start').value = timeToValue(timeMatch[1]);
|
||
slider.querySelector('.range-end').value = timeToValue(timeMatch[2]);
|
||
updateTimeRange(slider);
|
||
}
|
||
}
|
||
|
||
// Reset type to template (gentagelse)
|
||
document.querySelectorAll('#typeOptions swp-toggle-option').forEach(opt => {
|
||
opt.classList.toggle('selected', opt.dataset.value === 'template');
|
||
});
|
||
repeatGroup.style.display = 'block';
|
||
|
||
// Udfyld note hvis den findes
|
||
document.getElementById('fieldNote').value = cell.dataset.note || '';
|
||
}
|
||
|
||
function resetFormToDefault() {
|
||
// Reset to work status
|
||
document.querySelectorAll('#statusOptions swp-status-option').forEach(opt => {
|
||
opt.classList.toggle('selected', opt.dataset.status === 'work');
|
||
});
|
||
timeRow.style.display = 'flex';
|
||
|
||
// Reset time slider to 09:00-17:00
|
||
const slider = drawerTimeRange.querySelector('swp-time-range-slider');
|
||
slider.querySelector('.range-start').value = 12; // 09:00
|
||
slider.querySelector('.range-end').value = 44; // 17:00
|
||
updateTimeRange(slider);
|
||
|
||
// Reset type to template (gentagelse)
|
||
document.querySelectorAll('#typeOptions swp-toggle-option').forEach(opt => {
|
||
opt.classList.toggle('selected', opt.dataset.value === 'template');
|
||
});
|
||
repeatGroup.style.display = 'block';
|
||
|
||
// Nulstil note
|
||
document.getElementById('fieldNote').value = '';
|
||
}
|
||
|
||
function openDrawer() {
|
||
drawer.classList.add('open');
|
||
}
|
||
|
||
function closeDrawer() {
|
||
drawer.classList.remove('open');
|
||
}
|
||
|
||
function clearSelection() {
|
||
selectedCells.forEach(c => {
|
||
c.classList.remove('selected', 'adj-left', 'adj-right', 'adj-top', 'adj-bottom');
|
||
});
|
||
selectedCells = [];
|
||
}
|
||
|
||
drawerClose.addEventListener('click', () => {
|
||
// Exit edit mode
|
||
isEditMode = false;
|
||
document.body.classList.remove('edit-mode');
|
||
editModeBtn.innerHTML = `
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||
</svg>
|
||
Rediger`;
|
||
closeDrawer();
|
||
clearSelection();
|
||
});
|
||
|
||
drawerCancel.addEventListener('click', () => {
|
||
clearSelection();
|
||
showEmptyState();
|
||
});
|
||
|
||
drawerSave.addEventListener('click', () => {
|
||
if (selectedCells.length === 0) return;
|
||
|
||
const selectedStatus = document.querySelector('#statusOptions swp-status-option.selected');
|
||
const status = selectedStatus ? selectedStatus.dataset.status : 'work';
|
||
|
||
// Get time from slider
|
||
const slider = drawerTimeRange.querySelector('swp-time-range-slider');
|
||
const startVal = parseInt(slider.querySelector('.range-start').value);
|
||
const endVal = parseInt(slider.querySelector('.range-end').value);
|
||
const startTime = valueToTime(startVal);
|
||
const endTime = valueToTime(endVal);
|
||
|
||
const noteValue = document.getElementById('fieldNote').value.trim();
|
||
|
||
selectedCells.forEach(cell => {
|
||
const timeDisplay = cell.querySelector('swp-time-display');
|
||
if (!timeDisplay) return;
|
||
|
||
// Store original if not already stored (kun hvis cellen har en reel tid)
|
||
const hasRealTime = !timeDisplay.classList.contains('off') &&
|
||
!timeDisplay.classList.contains('vacation') &&
|
||
!timeDisplay.classList.contains('sick');
|
||
if (!cell.dataset.originalTime && hasRealTime) {
|
||
cell.dataset.originalTime = timeDisplay.textContent;
|
||
}
|
||
|
||
// Remove all status classes
|
||
timeDisplay.classList.remove('off', 'off-override', 'vacation', 'sick');
|
||
|
||
switch (status) {
|
||
case 'work':
|
||
const formattedTime = `${startTime} - ${endTime}`;
|
||
timeDisplay.textContent = formattedTime;
|
||
break;
|
||
case 'off':
|
||
timeDisplay.classList.add('off');
|
||
if (cell.dataset.originalTime) {
|
||
timeDisplay.classList.add('off-override');
|
||
timeDisplay.textContent = 'Fri';
|
||
} else {
|
||
timeDisplay.textContent = '—';
|
||
}
|
||
break;
|
||
case 'vacation':
|
||
timeDisplay.classList.add('vacation');
|
||
timeDisplay.textContent = 'Ferie';
|
||
break;
|
||
case 'sick':
|
||
timeDisplay.classList.add('sick');
|
||
timeDisplay.textContent = 'Syg';
|
||
break;
|
||
}
|
||
|
||
// Håndter note-ikon
|
||
let noteIcon = cell.querySelector('swp-note-icon');
|
||
if (noteValue) {
|
||
if (!noteIcon) {
|
||
noteIcon = document.createElement('swp-note-icon');
|
||
noteIcon.innerHTML = '<img src="icons/note-sticky.svg" alt="Note">';
|
||
cell.appendChild(noteIcon);
|
||
}
|
||
cell.dataset.note = noteValue;
|
||
} else {
|
||
if (noteIcon) {
|
||
noteIcon.remove();
|
||
}
|
||
delete cell.dataset.note;
|
||
}
|
||
|
||
// Håndter override-indikator (vis original tid når ændret)
|
||
let overrideEl = cell.querySelector('swp-override-original');
|
||
if (cell.dataset.originalTime) {
|
||
// Der er en original værdi - vis override
|
||
if (!overrideEl) {
|
||
overrideEl = document.createElement('swp-override-original');
|
||
cell.appendChild(overrideEl);
|
||
}
|
||
overrideEl.textContent = cell.dataset.originalTime;
|
||
}
|
||
|
||
// Mark as modified
|
||
cell.dataset.modified = 'true';
|
||
});
|
||
|
||
clearSelection();
|
||
showEmptyState();
|
||
});
|
||
|
||
// ==========================================
|
||
// CELL CLICK (in edit mode)
|
||
// ==========================================
|
||
const dayCells = Array.from(document.querySelectorAll('swp-schedule-cell.day'));
|
||
let anchorCell = null; // Første valgte celle til shift-selection
|
||
|
||
document.querySelectorAll('swp-schedule-cell.day').forEach(cell => {
|
||
// Dobbeltklik aktiverer edit-mode
|
||
cell.addEventListener('dblclick', (e) => {
|
||
if (!isEditMode) {
|
||
isEditMode = true;
|
||
document.body.classList.add('edit-mode');
|
||
editModeBtn.innerHTML = `
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||
<polyline points="7 3 7 8 15 8"/>
|
||
</svg>
|
||
Færdig`;
|
||
openDrawer();
|
||
|
||
// Vælg den dobbeltklikkede celle
|
||
clearSelection();
|
||
cell.classList.add('selected');
|
||
selectedCells = [cell];
|
||
anchorCell = cell;
|
||
updateAdjacentClasses();
|
||
updateDrawerFields();
|
||
}
|
||
});
|
||
|
||
cell.addEventListener('click', (e) => {
|
||
if (!isEditMode) return;
|
||
|
||
if (e.shiftKey && anchorCell) {
|
||
// Shift-click: vælg alle celler fra anchor til denne
|
||
const anchorIdx = dayCells.indexOf(anchorCell);
|
||
const targetIdx = dayCells.indexOf(cell);
|
||
const startIdx = Math.min(anchorIdx, targetIdx);
|
||
const endIdx = Math.max(anchorIdx, targetIdx);
|
||
|
||
// Beregn rækkevidde baseret på grid position
|
||
const anchorRow = Math.floor(anchorIdx / 7);
|
||
const anchorCol = anchorIdx % 7;
|
||
const targetRow = Math.floor(targetIdx / 7);
|
||
const targetCol = targetIdx % 7;
|
||
|
||
const minRow = Math.min(anchorRow, targetRow);
|
||
const maxRow = Math.max(anchorRow, targetRow);
|
||
const minCol = Math.min(anchorCol, targetCol);
|
||
const maxCol = Math.max(anchorCol, targetCol);
|
||
|
||
clearSelection();
|
||
|
||
// Vælg alle celler i rektanglet
|
||
dayCells.forEach((c, idx) => {
|
||
const row = Math.floor(idx / 7);
|
||
const col = idx % 7;
|
||
if (row >= minRow && row <= maxRow && col >= minCol && col <= maxCol) {
|
||
c.classList.add('selected');
|
||
selectedCells.push(c);
|
||
}
|
||
});
|
||
} else if (e.ctrlKey || e.metaKey) {
|
||
// Multi-select with Ctrl/Cmd
|
||
if (cell.classList.contains('selected')) {
|
||
cell.classList.remove('selected');
|
||
selectedCells = selectedCells.filter(c => c !== cell);
|
||
} else {
|
||
cell.classList.add('selected');
|
||
selectedCells.push(cell);
|
||
anchorCell = cell;
|
||
}
|
||
} else {
|
||
// Single click - clear previous and select this one
|
||
clearSelection();
|
||
cell.classList.add('selected');
|
||
selectedCells = [cell];
|
||
anchorCell = cell;
|
||
}
|
||
|
||
updateAdjacentClasses();
|
||
updateDrawerFields();
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// STATUS OPTIONS
|
||
// ==========================================
|
||
function updateSelectedCellsStatus() {
|
||
const selectedStatus = document.querySelector('#statusOptions swp-status-option.selected');
|
||
const status = selectedStatus ? selectedStatus.dataset.status : 'work';
|
||
|
||
const slider = drawerTimeRange.querySelector('swp-time-range-slider');
|
||
const startVal = parseInt(slider.querySelector('.range-start').value);
|
||
const endVal = parseInt(slider.querySelector('.range-end').value);
|
||
const startTime = valueToTime(startVal);
|
||
const endTime = valueToTime(endVal);
|
||
|
||
selectedCells.forEach(cell => {
|
||
const timeDisplay = cell.querySelector('swp-time-display');
|
||
if (!timeDisplay) return;
|
||
|
||
// Gem original tid hvis ikke allerede gemt
|
||
const hasRealTime = !timeDisplay.classList.contains('off') &&
|
||
!timeDisplay.classList.contains('vacation') &&
|
||
!timeDisplay.classList.contains('sick');
|
||
const currentText = timeDisplay.textContent;
|
||
const isRealTimeText = currentText.includes(':'); // Tjek om det er en tid
|
||
|
||
if (!cell.dataset.originalTime && (hasRealTime || isRealTimeText)) {
|
||
cell.dataset.originalTime = currentText;
|
||
}
|
||
|
||
// Fjern alle status klasser
|
||
timeDisplay.classList.remove('off', 'off-override', 'vacation', 'sick');
|
||
|
||
switch (status) {
|
||
case 'work':
|
||
timeDisplay.textContent = `${startTime} - ${endTime}`;
|
||
break;
|
||
case 'off':
|
||
timeDisplay.classList.add('off');
|
||
if (cell.dataset.originalTime) {
|
||
timeDisplay.classList.add('off-override');
|
||
timeDisplay.textContent = 'Fri';
|
||
} else {
|
||
timeDisplay.textContent = '—';
|
||
}
|
||
break;
|
||
case 'vacation':
|
||
timeDisplay.classList.add('vacation');
|
||
timeDisplay.textContent = 'Ferie';
|
||
break;
|
||
case 'sick':
|
||
timeDisplay.classList.add('sick');
|
||
timeDisplay.textContent = 'Syg';
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
document.querySelectorAll('#statusOptions swp-status-option').forEach(option => {
|
||
option.addEventListener('click', () => {
|
||
document.querySelectorAll('#statusOptions swp-status-option').forEach(o => o.classList.remove('selected'));
|
||
option.classList.add('selected');
|
||
|
||
const status = option.dataset.status;
|
||
const showTime = status === 'work';
|
||
timeRow.style.display = showTime ? 'flex' : 'none';
|
||
|
||
// Opdater celler i realtid
|
||
updateSelectedCellsStatus();
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// TYPE TOGGLE (Enkelt/Gentagelse)
|
||
// ==========================================
|
||
document.querySelectorAll('#typeOptions swp-toggle-option').forEach(option => {
|
||
option.addEventListener('click', () => {
|
||
document.querySelectorAll('#typeOptions swp-toggle-option').forEach(o => o.classList.remove('selected'));
|
||
option.classList.add('selected');
|
||
|
||
const isTemplate = option.dataset.value === 'template';
|
||
repeatGroup.style.display = isTemplate ? 'block' : 'none';
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// WEEK NAVIGATION
|
||
// ==========================================
|
||
document.getElementById('prevWeek').addEventListener('click', () => {
|
||
console.log('Previous week');
|
||
});
|
||
|
||
document.getElementById('nextWeek').addEventListener('click', () => {
|
||
console.log('Next week');
|
||
});
|
||
|
||
// ==========================================
|
||
// LUKKEDAGE (klik på dag-header)
|
||
// ==========================================
|
||
const scheduleTable = document.querySelector('swp-schedule-table');
|
||
const dayHeaders = document.querySelectorAll('swp-schedule-cell.header:not(.week-number)');
|
||
|
||
dayHeaders.forEach((header, index) => {
|
||
header.addEventListener('click', () => {
|
||
if (!isEditMode) return;
|
||
|
||
// Toggle lukket status på header
|
||
header.classList.toggle('closed');
|
||
|
||
// Find alle celler i denne kolonne (kolonne index + 1 pga. employee kolonne)
|
||
const columnIndex = index + 1; // 0-baseret, +1 for employee kolonne
|
||
|
||
// Opdater alle dag-celler i kolonnen
|
||
const allDayCells = document.querySelectorAll('swp-schedule-cell.day');
|
||
allDayCells.forEach(cell => {
|
||
const cellIndex = allCells.indexOf(cell);
|
||
const cellColumn = cellIndex % GRID_COLUMNS;
|
||
|
||
if (cellColumn === columnIndex) {
|
||
cell.classList.toggle('closed-day', header.classList.contains('closed'));
|
||
}
|
||
});
|
||
|
||
// Opdater has-closed class på tabellen
|
||
const hasAnyClosed = document.querySelector('swp-schedule-cell.header.closed');
|
||
scheduleTable.classList.toggle('has-closed', !!hasAnyClosed);
|
||
});
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|