1784 lines
60 KiB
HTML
1784 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>
|