Calendar/wwwroot/poc-ai-booking-optimizer.html
Janus C. H. Knudsen 2a066c6d14 Enhances AI booking optimization with smart slot recommendations
Implements AI-driven time slot selection algorithm for booking system

Adds intelligent slot scoring mechanism that considers:
- Minimizing calendar gaps
- Optimizing employee time utilization
- Providing recommended time slots for customers

Introduces new AI features across booking interfaces to improve scheduling efficiency
2026-01-02 21:19:10 +01:00

1009 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Booking Optimering - KARINA KNUDSEN®</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css">
<style>
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Regular.woff') format('woff'); font-weight: 400; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Medium.woff') format('woff'); font-weight: 500; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-SemiBold.woff') format('woff'); font-weight: 600; }
@font-face { font-family: 'Poppins'; src: url('fonts/Poppins-Bold.woff') format('woff'); font-weight: 700; }
:root {
--color-teal: #00897b;
--color-teal-light: #e0f2f1;
--color-surface: #ffffff;
--color-background: #f5f5f5;
--color-background-alt: #fafafa;
--color-border: #e0e0e0;
--color-text: #333;
--color-text-secondary: #666;
--color-text-muted: #999;
--color-green: #43a047;
--color-amber: #f59e0b;
--color-red: #e53935;
--color-purple: #8b5cf6;
--color-blue: #1976d2;
--font-family: 'Poppins', -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-family);
background: var(--color-background);
color: var(--color-text);
min-height: 100vh;
}
/* ==========================================
TOPBAR
========================================== */
swp-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}
swp-topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
swp-topbar-title {
display: flex;
align-items: center;
gap: 12px;
}
swp-topbar-title h1 {
font-size: 18px;
font-weight: 600;
}
swp-ai-badge-header {
display: inline-flex;
align-items: center;
gap: 6px;
background: linear-gradient(135deg, var(--color-purple) 0%, var(--color-blue) 100%);
color: white;
font-size: 11px;
font-weight: 600;
padding: 4px 10px;
border-radius: 20px;
}
swp-topbar-date {
font-size: 14px;
color: var(--color-text-secondary);
}
/* ==========================================
PAGE CONTAINER
========================================== */
swp-page-container {
display: block;
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
/* ==========================================
STATS GRID
========================================== */
swp-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
@media (max-width: 900px) {
swp-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
swp-stat-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 20px;
background: var(--color-surface);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
swp-stat-card.highlight {
background: linear-gradient(135deg, var(--color-teal) 0%, #00695c 100%);
color: white;
}
swp-stat-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
swp-stat-card.highlight swp-stat-label {
color: rgba(255,255,255,0.8);
}
swp-stat-value {
font-size: 28px;
font-weight: 700;
font-family: var(--font-mono);
}
swp-stat-change {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 500;
}
swp-stat-change.positive {
color: var(--color-green);
}
swp-stat-change.negative {
color: var(--color-red);
}
swp-stat-card.highlight swp-stat-change {
color: rgba(255,255,255,0.9);
}
/* ==========================================
MAIN GRID
========================================== */
swp-main-grid {
display: grid;
grid-template-columns: 1fr 340px;
gap: 24px;
}
@media (max-width: 1000px) {
swp-main-grid {
grid-template-columns: 1fr;
}
}
/* ==========================================
CARD
========================================== */
swp-card {
display: block;
background: var(--color-surface);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
margin-bottom: 24px;
}
swp-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border);
}
swp-card-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
font-weight: 600;
}
swp-card-title i {
font-size: 20px;
color: var(--color-teal);
}
swp-card-content {
display: block;
padding: 20px;
}
/* ==========================================
MINI CALENDAR
========================================== */
swp-mini-calendar {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
swp-mini-day {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 8px;
background: var(--color-background-alt);
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
swp-mini-day:hover {
background: var(--color-background);
}
swp-mini-day.today {
background: var(--color-teal-light);
border: 2px solid var(--color-teal);
}
swp-mini-day-name {
font-size: 11px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
}
swp-mini-day-date {
font-size: 16px;
font-weight: 600;
}
swp-mini-day-status {
display: flex;
gap: 3px;
}
swp-mini-day-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--color-green);
}
swp-mini-day-dot.gap {
background: var(--color-amber);
}
swp-mini-day-dot.critical {
background: var(--color-red);
}
/* ==========================================
GAPS LIST
========================================== */
swp-gaps-list {
display: flex;
flex-direction: column;
gap: 16px;
}
swp-gap-card {
display: block;
border: 1px solid var(--color-border);
border-radius: 10px;
overflow: hidden;
}
swp-gap-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: var(--color-background-alt);
border-bottom: 1px solid var(--color-border);
}
swp-gap-time {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
}
swp-gap-time i {
font-size: 18px;
color: var(--color-amber);
}
swp-gap-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary);
}
swp-gap-employee {
display: flex;
align-items: center;
gap: 6px;
}
swp-gap-employee-avatar {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--color-purple);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
}
swp-gap-revenue {
font-weight: 500;
color: var(--color-red);
}
swp-gap-content {
padding: 16px;
}
swp-gap-suggestions-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 12px;
}
swp-gap-suggestions-title i {
font-size: 16px;
color: var(--color-purple);
}
swp-suggestion-list {
display: flex;
flex-direction: column;
gap: 10px;
}
swp-suggestion-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: var(--color-background-alt);
border-radius: 8px;
}
swp-suggestion-info {
display: flex;
flex-direction: column;
gap: 2px;
}
swp-suggestion-customer {
font-size: 13px;
font-weight: 500;
}
swp-suggestion-detail {
font-size: 11px;
color: var(--color-text-secondary);
}
swp-flex-score {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11px;
font-weight: 500;
color: var(--color-green);
background: color-mix(in srgb, var(--color-green) 12%, transparent);
padding: 2px 8px;
border-radius: 4px;
}
swp-suggestion-action {
display: flex;
align-items: center;
gap: 8px;
}
swp-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
font-size: 12px;
font-weight: 500;
font-family: var(--font-family);
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
border: none;
}
swp-btn.primary {
background: var(--color-teal);
color: white;
}
swp-btn.primary:hover {
background: #00695c;
}
swp-btn.secondary {
background: var(--color-surface);
border: 1px solid var(--color-border);
color: var(--color-text);
}
swp-btn.secondary:hover {
background: var(--color-background);
}
swp-btn i {
font-size: 14px;
}
/* Waitlist suggestion */
swp-suggestion-item.waitlist {
border: 1px dashed var(--color-border);
background: transparent;
}
swp-waitlist-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-weight: 600;
color: var(--color-blue);
background: color-mix(in srgb, var(--color-blue) 12%, transparent);
padding: 2px 8px;
border-radius: 4px;
text-transform: uppercase;
}
/* ==========================================
SMS HISTORY
========================================== */
swp-sms-list {
display: flex;
flex-direction: column;
gap: 0;
}
swp-sms-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
swp-sms-item:last-child {
border-bottom: none;
}
swp-sms-status-icon {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
swp-sms-status-icon.accepted {
background: color-mix(in srgb, var(--color-green) 15%, transparent);
color: var(--color-green);
}
swp-sms-status-icon.rejected {
background: color-mix(in srgb, var(--color-red) 15%, transparent);
color: var(--color-red);
}
swp-sms-status-icon.pending {
background: color-mix(in srgb, var(--color-amber) 15%, transparent);
color: var(--color-amber);
}
swp-sms-info {
flex: 1;
}
swp-sms-customer {
font-size: 13px;
font-weight: 500;
}
swp-sms-detail {
font-size: 11px;
color: var(--color-text-secondary);
}
swp-sms-date {
font-size: 11px;
color: var(--color-text-muted);
}
/* ==========================================
OPTIMIZATION SCORE
========================================== */
swp-optimization-score {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 24px;
text-align: center;
}
swp-score-circle {
width: 100px;
height: 100px;
border-radius: 50%;
background: conic-gradient(var(--color-teal) var(--score-percent), var(--color-border) var(--score-percent));
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
swp-score-circle::before {
content: '';
position: absolute;
width: 80px;
height: 80px;
border-radius: 50%;
background: var(--color-surface);
}
swp-score-value {
position: relative;
z-index: 1;
font-size: 24px;
font-weight: 700;
font-family: var(--font-mono);
color: var(--color-teal);
}
swp-score-label {
font-size: 13px;
color: var(--color-text-secondary);
}
swp-score-hint {
font-size: 11px;
color: var(--color-text-muted);
max-width: 200px;
}
/* ==========================================
SIDEBAR SECTION
========================================== */
swp-sidebar {
display: flex;
flex-direction: column;
gap: 24px;
}
</style>
</head>
<body>
<swp-topbar>
<swp-topbar-left>
<swp-topbar-title>
<h1>AI Booking Optimering</h1>
<swp-ai-badge-header>
<i class="ph ph-sparkle"></i>
AI-drevet
</swp-ai-badge-header>
</swp-topbar-title>
</swp-topbar-left>
<swp-topbar-date id="currentDate"></swp-topbar-date>
</swp-topbar>
<swp-page-container>
<!-- Stats Grid -->
<swp-stats-grid>
<swp-stat-card>
<swp-stat-label>Huller i dag</swp-stat-label>
<swp-stat-value id="gapsToday">3</swp-stat-value>
<swp-stat-change class="negative">
<i class="ph ph-arrow-up"></i>
+1 fra i går
</swp-stat-change>
</swp-stat-card>
<swp-stat-card>
<swp-stat-label>Tabt omsætning</swp-stat-label>
<swp-stat-value id="lostRevenue">1.950 kr.</swp-stat-value>
<swp-stat-change class="negative">
<i class="ph ph-arrow-up"></i>
+450 kr.
</swp-stat-change>
</swp-stat-card>
<swp-stat-card>
<swp-stat-label>Huller denne uge</swp-stat-label>
<swp-stat-value id="gapsWeek">12</swp-stat-value>
<swp-stat-change class="positive">
<i class="ph ph-arrow-down"></i>
-3 fra sidste uge
</swp-stat-change>
</swp-stat-card>
<swp-stat-card class="highlight">
<swp-stat-label>Potentiel besparelse</swp-stat-label>
<swp-stat-value id="potentialSavings">8.400 kr.</swp-stat-value>
<swp-stat-change>
<i class="ph ph-trend-up"></i>
Denne måned
</swp-stat-change>
</swp-stat-card>
</swp-stats-grid>
<!-- Main Grid -->
<swp-main-grid>
<!-- Left Column -->
<div>
<!-- Mini Calendar -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-calendar"></i>
Denne uge
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-mini-calendar id="miniCalendar"></swp-mini-calendar>
</swp-card-content>
</swp-card>
<!-- Gaps List -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-warning-circle"></i>
Identificerede huller
</swp-card-title>
<swp-btn class="secondary">
<i class="ph ph-funnel"></i>
Filter
</swp-btn>
</swp-card-header>
<swp-card-content>
<swp-gaps-list id="gapsList"></swp-gaps-list>
</swp-card-content>
</swp-card>
</div>
<!-- Sidebar -->
<swp-sidebar>
<!-- Optimization Score -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-chart-pie"></i>
Optimeringsscore
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-optimization-score>
<swp-score-circle style="--score-percent: 78%;">
<swp-score-value>78%</swp-score-value>
</swp-score-circle>
<swp-score-label>Kalenderudnyttelse</swp-score-label>
<swp-score-hint>Mål: 90% udnyttelse. Fyld 2 huller mere for at nå målet.</swp-score-hint>
</swp-optimization-score>
</swp-card-content>
</swp-card>
<!-- SMS History -->
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-chat-circle-text"></i>
SMS-historik
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-sms-list id="smsList"></swp-sms-list>
</swp-card-content>
</swp-card>
</swp-sidebar>
</swp-main-grid>
</swp-page-container>
<script>
// ==========================================
// DATA
// ==========================================
const employees = [
{ id: 'EMP001', name: 'Camilla', initials: 'CA', color: '#9c27b0' },
{ id: 'EMP002', name: 'Isabella', initials: 'IS', color: '#e91e63' },
{ id: 'EMP003', name: 'Alexander', initials: 'AL', color: '#3f51b5' },
{ id: 'EMP004', name: 'Viktor', initials: 'VI', color: '#009688' }
];
const gaps = [
{
id: 'gap1',
date: '2026-01-06',
dayName: 'Tirsdag',
start: '11:00',
end: '12:00',
duration: 60,
employeeId: 'EMP001',
lostRevenue: 650,
suggestions: [
{
type: 'move',
customerId: 'C001',
customerName: 'Maria Jensen',
currentTime: '14:00',
currentDate: '2026-01-06',
flexScore: 87,
service: 'Dameklip'
},
{
type: 'waitlist',
customerId: 'C002',
customerName: 'Lars Hansen',
note: 'Ønsker tid denne uge',
service: 'Herreklip'
}
]
},
{
id: 'gap2',
date: '2026-01-06',
dayName: 'Tirsdag',
start: '14:30',
end: '15:30',
duration: 60,
employeeId: 'EMP002',
lostRevenue: 725,
suggestions: [
{
type: 'move',
customerId: 'C003',
customerName: 'Anne Larsen',
currentTime: '10:00',
currentDate: '2026-01-07',
flexScore: 62,
service: 'Dameklip'
}
]
},
{
id: 'gap3',
date: '2026-01-08',
dayName: 'Torsdag',
start: '09:00',
end: '10:30',
duration: 90,
employeeId: 'EMP003',
lostRevenue: 575,
suggestions: [
{
type: 'waitlist',
customerId: 'C004',
customerName: 'Peter Olsen',
note: 'Ny kunde, fleksibel',
service: 'Bundfarve'
}
]
}
];
const weekDays = [
{ date: '2026-01-05', name: 'Man', day: 5, gaps: 0, bookings: 8 },
{ date: '2026-01-06', name: 'Tir', day: 6, gaps: 2, bookings: 6, isToday: true },
{ date: '2026-01-07', name: 'Ons', day: 7, gaps: 1, bookings: 7 },
{ date: '2026-01-08', name: 'Tor', day: 8, gaps: 1, bookings: 5 },
{ date: '2026-01-09', name: 'Fre', day: 9, gaps: 0, bookings: 9 }
];
const smsHistory = [
{
customerId: 'C005',
customerName: 'Sofie Nielsen',
status: 'accepted',
action: 'Flyttede fra 15:00 til 11:00',
discount: '5%',
date: 'I går'
},
{
customerId: 'C006',
customerName: 'Peter Olsen',
status: 'rejected',
action: 'Afviste flytning',
date: '3. jan'
},
{
customerId: 'C003',
customerName: 'Anne Larsen',
status: 'pending',
action: 'Tilbud sendt: Flyt til 14:30',
discount: '5%',
date: 'I dag'
},
{
customerId: 'C007',
customerName: 'Michael Bruun',
status: 'accepted',
action: 'Booket fra venteliste',
date: '2. jan'
}
];
// ==========================================
// RENDER FUNCTIONS
// ==========================================
function getEmployee(id) {
return employees.find(e => e.id === id) || employees[0];
}
function renderCurrentDate() {
const now = new Date();
const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' };
document.getElementById('currentDate').textContent = now.toLocaleDateString('da-DK', options);
}
function renderMiniCalendar() {
const container = document.getElementById('miniCalendar');
container.innerHTML = weekDays.map(day => `
<swp-mini-day class="${day.isToday ? 'today' : ''}">
<swp-mini-day-name>${day.name}</swp-mini-day-name>
<swp-mini-day-date>${day.day}</swp-mini-day-date>
<swp-mini-day-status>
${day.gaps > 0 ? `<swp-mini-day-dot class="${day.gaps > 1 ? 'critical' : 'gap'}"></swp-mini-day-dot>`.repeat(Math.min(day.gaps, 3)) : '<swp-mini-day-dot></swp-mini-day-dot>'}
</swp-mini-day-status>
</swp-mini-day>
`).join('');
}
function renderGapsList() {
const container = document.getElementById('gapsList');
container.innerHTML = gaps.map(gap => {
const emp = getEmployee(gap.employeeId);
return `
<swp-gap-card>
<swp-gap-header>
<swp-gap-time>
<i class="ph ph-clock"></i>
${gap.dayName} ${gap.start}-${gap.end} (${gap.duration} min)
</swp-gap-time>
<swp-gap-meta>
<swp-gap-employee>
<swp-gap-employee-avatar style="background: ${emp.color}">${emp.initials}</swp-gap-employee-avatar>
${emp.name}
</swp-gap-employee>
<swp-gap-revenue>~${gap.lostRevenue} kr. tabt</swp-gap-revenue>
</swp-gap-meta>
</swp-gap-header>
<swp-gap-content>
<swp-gap-suggestions-title>
<i class="ph ph-sparkle"></i>
AI-forslag
</swp-gap-suggestions-title>
<swp-suggestion-list>
${gap.suggestions.map(s => renderSuggestion(s)).join('')}
</swp-suggestion-list>
</swp-gap-content>
</swp-gap-card>
`;
}).join('');
}
function renderSuggestion(suggestion) {
if (suggestion.type === 'move') {
return `
<swp-suggestion-item>
<swp-suggestion-info>
<swp-suggestion-customer>${suggestion.customerName}</swp-suggestion-customer>
<swp-suggestion-detail>Nuværende tid: ${suggestion.currentTime} · ${suggestion.service}</swp-suggestion-detail>
<swp-flex-score>
<i class="ph ph-chart-line-up"></i>
Fleksibilitet: ${suggestion.flexScore}%
</swp-flex-score>
</swp-suggestion-info>
<swp-suggestion-action>
<swp-btn class="primary" onclick="sendOffer('${suggestion.customerId}')">
<i class="ph ph-paper-plane-tilt"></i>
Send tilbud -5%
</swp-btn>
</swp-suggestion-action>
</swp-suggestion-item>
`;
} else {
return `
<swp-suggestion-item class="waitlist">
<swp-suggestion-info>
<swp-suggestion-customer>
${suggestion.customerName}
<swp-waitlist-badge>
<i class="ph ph-clock"></i>
Venteliste
</swp-waitlist-badge>
</swp-suggestion-customer>
<swp-suggestion-detail>${suggestion.note} · ${suggestion.service}</swp-suggestion-detail>
</swp-suggestion-info>
<swp-suggestion-action>
<swp-btn class="secondary" onclick="sendOffer('${suggestion.customerId}')">
<i class="ph ph-paper-plane-tilt"></i>
Send tilbud
</swp-btn>
</swp-suggestion-action>
</swp-suggestion-item>
`;
}
}
function renderSmsHistory() {
const container = document.getElementById('smsList');
container.innerHTML = smsHistory.map(sms => {
let icon, iconClass;
if (sms.status === 'accepted') {
icon = 'check';
iconClass = 'accepted';
} else if (sms.status === 'rejected') {
icon = 'x';
iconClass = 'rejected';
} else {
icon = 'hourglass';
iconClass = 'pending';
}
return `
<swp-sms-item>
<swp-sms-status-icon class="${iconClass}">
<i class="ph ph-${icon}"></i>
</swp-sms-status-icon>
<swp-sms-info>
<swp-sms-customer>${sms.customerName}</swp-sms-customer>
<swp-sms-detail>${sms.action}${sms.discount ? ` · ${sms.discount} rabat` : ''}</swp-sms-detail>
</swp-sms-info>
<swp-sms-date>${sms.date}</swp-sms-date>
</swp-sms-item>
`;
}).join('');
}
// ==========================================
// ACTIONS
// ==========================================
function sendOffer(customerId) {
// Simuler send
alert(`SMS sendt til kunde ${customerId} med tilbud om at flytte tid!`);
}
// ==========================================
// INIT
// ==========================================
function init() {
renderCurrentDate();
renderMiniCalendar();
renderGapsList();
renderSmsHistory();
}
init();
</script>
</body>
</html>