diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index fc2387f..3200db8 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -18,7 +18,10 @@
"Bash(powershell -Command \"Get-ChildItem -Path src -Filter ''index.ts'' -Recurse | Select-Object -ExpandProperty FullName\")",
"Bash(powershell -Command:*)",
"WebFetch(domain:www.npmjs.com)",
- "WebFetch(domain:unpkg.com)"
+ "WebFetch(domain:unpkg.com)",
+ "Bash(node -e:*)",
+ "Bash(ls:*)",
+ "Bash(find:*)"
],
"deny": [],
"ask": []
diff --git a/.workbench/Screenshot_1.png b/.workbench/Screenshot_1.png
deleted file mode 100644
index 8886c23..0000000
Binary files a/.workbench/Screenshot_1.png and /dev/null differ
diff --git a/.workbench/Screenshot_2.png b/.workbench/Screenshot_2.png
deleted file mode 100644
index 1acf8d4..0000000
Binary files a/.workbench/Screenshot_2.png and /dev/null differ
diff --git a/.workbench/Screenshot_3.png b/.workbench/Screenshot_3.png
deleted file mode 100644
index e3e1068..0000000
Binary files a/.workbench/Screenshot_3.png and /dev/null differ
diff --git a/.workbench/Screenshot_4.png b/.workbench/Screenshot_4.png
deleted file mode 100644
index 0b59286..0000000
Binary files a/.workbench/Screenshot_4.png and /dev/null differ
diff --git a/.workbench/Screenshot_5.png b/.workbench/Screenshot_5.png
deleted file mode 100644
index 25f5b39..0000000
Binary files a/.workbench/Screenshot_5.png and /dev/null differ
diff --git a/.workbench/Screenshot_6.png b/.workbench/Screenshot_6.png
deleted file mode 100644
index 4770812..0000000
Binary files a/.workbench/Screenshot_6.png and /dev/null differ
diff --git a/.workbench/Screenshot_7.png b/.workbench/Screenshot_7.png
deleted file mode 100644
index bcf25ad..0000000
Binary files a/.workbench/Screenshot_7.png and /dev/null differ
diff --git a/.workbench/StandardMapping.xlsx b/.workbench/StandardMapping.xlsx
new file mode 100644
index 0000000..55e496a
Binary files /dev/null and b/.workbench/StandardMapping.xlsx differ
diff --git a/package-lock.json b/package-lock.json
index aaf69a2..d0ad451 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,10 +33,12 @@
"postcss-cli": "^11.0.1",
"postcss-nesting": "^13.0.2",
"purgecss": "^7.0.2",
+ "read-excel-file": "^6.0.1",
"rollup": "^4.52.5",
"tslib": "^2.8.1",
"typescript": "^5.0.0",
- "vitest": "^3.2.4"
+ "vitest": "^3.2.4",
+ "xlsx": "^0.18.5"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -1348,6 +1350,16 @@
"url": "https://opencollective.com/vitest"
}
},
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.8.11",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+ "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -1360,6 +1372,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -1510,6 +1532,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -1618,6 +1647,20 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@@ -1781,6 +1824,16 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1834,6 +1887,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2310,6 +2383,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/duplexer2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+ "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "readable-stream": "^2.0.2"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2581,6 +2664,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2858,6 +2951,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -3036,6 +3136,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3325,6 +3432,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4289,6 +4403,13 @@
"node": ">= 0.8"
}
},
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/pseudo-classes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pseudo-classes/-/pseudo-classes-1.0.0.tgz",
@@ -4339,6 +4460,34 @@
"pify": "^2.3.0"
}
},
+ "node_modules/read-excel-file": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-6.0.1.tgz",
+ "integrity": "sha512-rH6huBFxsjZsUARCYh55O08cn1gqZH8bnLf0kI6y5K7+9yqBVzy8veO4gPV4VGKv4M9rdcRtXTDGZZNwPi1gDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.8.11",
+ "fflate": "^0.8.2",
+ "unzipper": "^0.12.3"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
@@ -4479,6 +4628,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -4597,6 +4753,19 @@
"specificity": "bin/specificity"
}
},
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -4611,6 +4780,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -5048,6 +5227,27 @@
"node": ">=18.12.0"
}
},
+ "node_modules/unzipper": {
+ "version": "0.12.3",
+ "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz",
+ "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bluebird": "~3.7.2",
+ "duplexer2": "~0.1.4",
+ "fs-extra": "^11.2.0",
+ "graceful-fs": "^4.2.2",
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/unzipper/node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
@@ -5796,6 +5996,26 @@
"node": ">=8"
}
},
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
@@ -5916,6 +6136,28 @@
}
}
},
+ "node_modules/xlsx": {
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
diff --git a/package.json b/package.json
index 8eb633b..36c19f2 100644
--- a/package.json
+++ b/package.json
@@ -33,10 +33,12 @@
"postcss-cli": "^11.0.1",
"postcss-nesting": "^13.0.2",
"purgecss": "^7.0.2",
+ "read-excel-file": "^6.0.1",
"rollup": "^4.52.5",
"tslib": "^2.8.1",
"typescript": "^5.0.0",
- "vitest": "^3.2.4"
+ "vitest": "^3.2.4",
+ "xlsx": "^0.18.5"
},
"dependencies": {
"@novadi/core": "^0.6.0",
diff --git a/wwwroot/docs/ai-booking-optimering.md b/wwwroot/docs/ai-booking-optimering.md
new file mode 100644
index 0000000..1312d50
--- /dev/null
+++ b/wwwroot/docs/ai-booking-optimering.md
@@ -0,0 +1,255 @@
+# AI Booking Optimering
+
+## Produktoversigt
+
+AI Booking Optimering er et intelligent tillægsmodul der hjælper saloner med at maksimere deres kalenderudnyttelse og reducere tabt omsætning fra tomme tidsslots.
+
+---
+
+## Features
+
+### Feature 1: Smart Tidsforslag (Real-time)
+
+Når kunder booker online, analyserer AI'en eksisterende bookinger og foreslår de mest optimale tidspunkter.
+
+**Hvordan det virker:**
+1. Kunden vælger ydelse (f.eks. Dameklip, 60 min)
+2. AI'en analyserer dagens/ugens bookinger for den valgte medarbejder
+3. Hvert ledigt tidsslot får en score baseret på:
+ - Minimering af huller
+ - Optimal udnyttelse af åbningstiden
+ - Kontinuitet i bookinger
+4. Top 2-3 bedste slots markeres med "Anbefalet" badge
+
+**Scoring-algoritme:**
+
+| Kriterium | Score |
+|-----------|-------|
+| Starter ved åbningstid | +3 |
+| Slutter præcis på næste booking | +3 |
+| Starter lige efter en booking | +2 |
+| Slutter ved lukketid | +1 |
+| Skaber hul < 30 min | -2 |
+
+**UX-principper:**
+- Blød anbefaling - kunden kan stadig vælge alle ledige tider
+- Grøn badge med AI-ikon på anbefalede tider
+- Info-tekst forklarer fordelen
+
+---
+
+### Feature 2: Kalender-optimering Dashboard
+
+Salonejere får et dashboard der identificerer huller og foreslår handlinger.
+
+**Dashboard-komponenter:**
+
+1. **Statistik-kort**
+ - Huller i dag
+ - Tabt omsætning (estimeret)
+ - Huller denne uge
+ - Potentiel besparelse
+
+2. **Mini-kalender**
+ - Visuel oversigt over ugen
+ - Farvekodede dage (grøn = optimalt, gul = huller, rød = kritisk)
+
+3. **Hul-liste**
+ - Detaljeret visning af hvert identificeret hul
+ - Medarbejder og tidspunkt
+ - Estimeret tabt omsætning
+ - AI-forslag til at fylde hullet
+
+4. **AI-forslag typer:**
+ - **Flyt booking**: Foreslå at flytte en eksisterende kundes tid
+ - **Venteliste**: Kontakt kunde fra ventelisten
+ - **Rabattilbud**: Send SMS med rabat for at fylde hullet
+
+5. **SMS-historik**
+ - Track sendte tilbud
+ - Accept/afvisning statistik
+ - Pending tilbud
+
+---
+
+## Teknisk Implementation
+
+### POC 1: poc-booking-v2.html
+
+**Tilføjede komponenter:**
+
+```javascript
+// Mock data for eksisterende bookinger
+const existingBookings = {
+ 'EMP001': {
+ '2026-01-06': [
+ { start: '10:00', end: '11:00', service: 'Dameklip' },
+ { start: '13:30', end: '14:30', service: 'Herreklip' }
+ ]
+ }
+};
+
+// Scoring-algoritme
+function calculateOptimalSlots(serviceDuration, date, employeeId) {
+ // 1. Hent bookinger for dato/medarbejder
+ // 2. Generer alle mulige slots (30 min intervaller)
+ // 3. Tjek overlap med eksisterende bookinger
+ // 4. Beregn score for hvert ledigt slot
+ // 5. Marker top 3 med positiv score som "recommended"
+ return slots;
+}
+```
+
+**CSS-styling:**
+- `.time-slot.recommended` - Grøn border og baggrund
+- `.ai-badge` - Absolut positioneret badge med sparkle-ikon
+- `.ai-info` - Info-boks over tidsgrid
+
+### POC 2: poc-ai-booking-optimizer.html
+
+**Struktur:**
+- Topbar med AI-badge
+- Stats-grid med 4 KPI-kort
+- Main-grid med kalender og hul-liste
+- Sidebar med optimeringsscore og SMS-historik
+
+**Mock data:**
+- `gaps[]` - Identificerede huller med forslag
+- `weekDays[]` - Ugevisning med gap-status
+- `smsHistory[]` - Historik over sendte tilbud
+
+---
+
+## Forretningsværdi
+
+### ROI-beregning
+
+```
+Typisk salon:
+- 4 medarbejdere
+- 40 timer/uge pr. medarbejder
+- 15% tomme slots = 24 timer/uge tabt
+- Gennemsnitlig timepris: 500 kr.
+- Tabt omsætning: 12.000 kr./uge = 624.000 kr./år
+
+AI-optimering fylder 50% af huller:
+- Ekstra omsætning: 312.000 kr./år
+
+Pris for AI-modul: 499 kr./md = 5.988 kr./år
+ROI: 52x investering
+```
+
+### Nøgletal at tracke
+
+| Metrik | Beskrivelse |
+|--------|-------------|
+| Kalenderudnyttelse | % af tilgængelige timer der er booket |
+| Gennemsnitligt hul | Minutter tabt pr. dag i gaps |
+| Accept-rate | % af kunder der accepterer flyttetilbud |
+| Tabt omsætning | Estimeret kr. i tomme slots |
+| Optimeringsscore | Samlet effektivitet (mål: 90%+) |
+
+---
+
+## Fremtidig AI-udvidelse
+
+### Niveau 1: Regelbaseret (Nuværende POC)
+- Statiske scoring-regler
+- Ingen læring
+- Fungerer for alle saloner ens
+
+### Niveau 2: Machine Learning
+- Lærer fra salonens historik
+- Personlige kundeprofilenr
+- Forudsigelse af no-shows
+- Dynamisk prisjustering
+
+### ML-features (fremtidig):
+
+1. **Historisk mønstergenkendelse**
+ - Populære vs. døde tider
+ - Sæsonvariation
+ - Service-specifikke mønstre
+
+2. **Kundesegmentering**
+ - Fleksible vs. fastlåste kunder
+ - Pris-sensitive kunder
+ - No-show risiko-profiler
+
+3. **Intelligent rabat-beregning**
+ - Dynamisk rabat baseret på:
+ - Hullets "værdi"
+ - Kundens prissensitivitet
+ - Sandsynlighed for naturlig booking
+
+4. **Proaktiv optimering**
+ - Forudsig huller før de opstår
+ - Automatisk udsend tilbud
+ - Venteliste-matching
+
+---
+
+## Integration med eksisterende system
+
+### Data-flow
+
+```
+Booking system
+ │
+ ▼
+┌─────────────────┐
+│ AI Optimizer │
+│ - Analyse │
+│ - Scoring │
+│ - Anbefalinger │
+└─────────────────┘
+ │
+ ▼
+┌─────────────────┐ ┌─────────────────┐
+│ Booking Widget │ │ Dashboard │
+│ (kundevisning) │ │ (ejervisning) │
+└─────────────────┘ └─────────────────┘
+```
+
+### API-endpoints (fremtidig)
+
+```
+GET /api/ai/optimal-slots?date=X&employee=Y&duration=Z
+POST /api/ai/send-offer
+GET /api/ai/gaps?week=X
+GET /api/ai/stats
+```
+
+---
+
+## Konfiguration
+
+### Indstillinger pr. salon
+
+| Indstilling | Beskrivelse | Default |
+|-------------|-------------|---------|
+| `minGapMinutes` | Mindste hul der tæller som tabt | 30 min |
+| `recommendedSlots` | Antal anbefalede slots | 3 |
+| `defaultDiscount` | Standard rabat ved flytning | 5% |
+| `autoSendOffers` | Automatisk udsend tilbud | Fra |
+| `smsEnabled` | Aktiver SMS-udsendelse | Til |
+
+---
+
+## Filer
+
+| Fil | Beskrivelse |
+|-----|-------------|
+| `poc-booking-v2.html` | Kundens booking-widget med AI-anbefalinger |
+| `poc-ai-booking-optimizer.html` | Dashboard til salonejere |
+| `docs/ai-booking-optimering.md` | Denne dokumentation |
+
+---
+
+## Changelog
+
+### Version 1.0 (Januar 2026)
+- Initial POC implementation
+- Regelbaseret scoring-algoritme
+- Dashboard med hul-identifikation
+- SMS-historik tracking
diff --git a/wwwroot/poc-ai-booking-optimizer.html b/wwwroot/poc-ai-booking-optimizer.html
new file mode 100644
index 0000000..65fa4ff
--- /dev/null
+++ b/wwwroot/poc-ai-booking-optimizer.html
@@ -0,0 +1,1009 @@
+
+
+
+
+
+ AI Booking Optimering - KARINA KNUDSEN®
+
+
+
+
+
+
+
+
+
+ AI Booking Optimering
+
+
+ AI-drevet
+
+
+
+
+
+
+
+
+
+
+ Huller i dag
+ 3
+
+
+ +1 fra i går
+
+
+
+
+ Tabt omsætning
+ 1.950 kr.
+
+
+ +450 kr.
+
+
+
+
+ Huller denne uge
+ 12
+
+
+ -3 fra sidste uge
+
+
+
+
+ Potentiel besparelse
+ 8.400 kr.
+
+
+ Denne måned
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Denne uge
+
+
+
+
+
+
+
+
+
+
+
+
+ Identificerede huller
+
+
+
+ Filter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Optimeringsscore
+
+
+
+
+
+ 78%
+
+ Kalenderudnyttelse
+ Mål: 90% udnyttelse. Fyld 2 huller mere for at nå målet.
+
+
+
+
+
+
+
+
+
+ SMS-historik
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/wwwroot/poc-booking-v2.html b/wwwroot/poc-booking-v2.html
index fef90fb..f3c71bc 100644
--- a/wwwroot/poc-booking-v2.html
+++ b/wwwroot/poc-booking-v2.html
@@ -504,6 +504,58 @@
text-decoration: line-through;
}
+ /* AI Recommended Slots */
+ .time-slot.recommended {
+ position: relative;
+ border: 2px solid var(--color-green);
+ background: color-mix(in srgb, var(--color-green) 8%, white);
+ }
+
+ .time-slot.recommended:hover:not(.disabled):not(.selected) {
+ background: color-mix(in srgb, var(--color-green) 15%, white);
+ }
+
+ .time-slot.recommended.selected {
+ background: var(--color-green);
+ border-color: var(--color-green);
+ }
+
+ .ai-badge {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ background: var(--color-green);
+ color: white;
+ font-size: 10px;
+ padding: 2px 5px;
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ font-weight: 500;
+ font-family: var(--font-family);
+ }
+
+ .ai-badge i {
+ font-size: 10px;
+ }
+
+ .ai-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ background: color-mix(in srgb, var(--color-green) 10%, white);
+ border-radius: 8px;
+ margin-bottom: 12px;
+ font-size: 12px;
+ color: var(--color-green);
+ }
+
+ .ai-info i {
+ font-size: 16px;
+ }
+
/* ==========================================
WAITLIST
========================================== */
@@ -1917,6 +1969,133 @@
{ id: "EMP004", name: "Viktor", role: "Junior Stylist", color: "#009688", priceModifier: -100 }
];
+ // ==========================================
+ // EKSISTERENDE BOOKINGER (Mock data til AI-optimering)
+ // ==========================================
+ const existingBookings = {
+ 'EMP001': {
+ '2026-01-06': [
+ { start: '10:00', end: '11:00', service: 'Dameklip' },
+ { start: '13:30', end: '14:30', service: 'Herreklip' }
+ ],
+ '2026-01-07': [
+ { start: '09:00', end: '10:30', service: 'Bundfarve' },
+ { start: '11:00', end: '12:00', service: 'Dameklip' },
+ { start: '14:00', end: '15:00', service: 'Dameklip' }
+ ]
+ },
+ 'EMP002': {
+ '2026-01-06': [
+ { start: '09:00', end: '10:00', service: 'Herreklip' },
+ { start: '11:00', end: '12:00', service: 'Dameklip' },
+ { start: '15:00', end: '16:30', service: 'Striber' }
+ ],
+ '2026-01-07': [
+ { start: '10:00', end: '11:00', service: 'Dameklip' },
+ { start: '13:00', end: '14:00', service: 'Herreklip' }
+ ]
+ },
+ 'EMP003': {
+ '2026-01-06': [
+ { start: '08:30', end: '09:30', service: 'Herreklip' },
+ { start: '12:00', end: '13:00', service: 'Dameklip' }
+ ]
+ },
+ 'EMP004': {
+ '2026-01-06': [
+ { start: '09:00', end: '10:00', service: 'Herreklip' },
+ { start: '14:00', end: '15:30', service: 'Bundfarve' }
+ ]
+ }
+ };
+
+ const SALON_OPEN = '08:00';
+ const SALON_CLOSE = '17:00';
+
+ // ==========================================
+ // AI SLOT OPTIMERING
+ // ==========================================
+ function timeToMinutes(time) {
+ const [h, m] = time.split(':').map(Number);
+ return h * 60 + m;
+ }
+
+ function minutesToTime(mins) {
+ const h = Math.floor(mins / 60);
+ const m = mins % 60;
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
+ }
+
+ function getBookingsForSlot(date, employeeId) {
+ if (!employeeId) {
+ let bestEmployee = 'EMP001';
+ let minBookings = Infinity;
+ for (const empId of Object.keys(existingBookings)) {
+ const bookings = existingBookings[empId]?.[date] || [];
+ if (bookings.length < minBookings) {
+ minBookings = bookings.length;
+ bestEmployee = empId;
+ }
+ }
+ return existingBookings[bestEmployee]?.[date] || [];
+ }
+ return existingBookings[employeeId]?.[date] || [];
+ }
+
+ function calculateOptimalSlots(serviceDuration, date, employeeId) {
+ const bookings = getBookingsForSlot(date, employeeId);
+ const openMins = timeToMinutes(SALON_OPEN);
+ const closeMins = timeToMinutes(SALON_CLOSE);
+
+ const slots = [];
+ for (let mins = openMins; mins <= closeMins - serviceDuration; mins += 30) {
+ const slotStart = mins;
+ const slotEnd = mins + serviceDuration;
+ const time = minutesToTime(mins);
+
+ let isTaken = false;
+ for (const booking of bookings) {
+ const bookStart = timeToMinutes(booking.start);
+ const bookEnd = timeToMinutes(booking.end);
+ if (slotStart < bookEnd && slotEnd > bookStart) {
+ isTaken = true;
+ break;
+ }
+ }
+
+ let score = 0;
+ if (!isTaken) {
+ if (slotStart === openMins) score += 3;
+ for (const booking of bookings) {
+ if (slotEnd === timeToMinutes(booking.start)) { score += 3; break; }
+ }
+ for (const booking of bookings) {
+ if (slotStart === timeToMinutes(booking.end)) { score += 2; break; }
+ }
+ for (const booking of bookings) {
+ const gap = timeToMinutes(booking.start) - slotEnd;
+ if (gap > 0 && gap < 30) { score -= 2; break; }
+ }
+ for (const booking of bookings) {
+ const gap = slotStart - timeToMinutes(booking.end);
+ if (gap > 0 && gap < 30) { score -= 2; break; }
+ }
+ if (slotEnd >= closeMins - 60) score += 1;
+ }
+
+ slots.push({ time, taken: isTaken, score, recommended: false });
+ }
+
+ const availableSlots = slots.filter(s => !s.taken);
+ availableSlots.sort((a, b) => b.score - a.score);
+ const topSlots = availableSlots.slice(0, 3);
+ for (const slot of topSlots) {
+ if (slot.score > 0) slot.recommended = true;
+ }
+
+ return slots;
+ }
+
// ==========================================
// STEP ANIMATION
// ==========================================
@@ -2196,8 +2375,18 @@
daysHtml += `${d}
`;
}
- const times = ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30'];
- const taken = ['10:30', '14:00'];
+ // Beregn total varighed for valgte ydelser
+ const serviceDuration = state.services.reduce((sum, s) => sum + s.duration, 0) || 60;
+
+ // Brug AI-algoritme til at beregne optimale slots
+ const selectedDate = state.date || new Date().toISOString().split('T')[0];
+ const slots = calculateOptimalSlots(serviceDuration, selectedDate, state.employee);
+
+ // Filtrer til åbningstider (08:00-17:00)
+ const displaySlots = slots.filter(s => {
+ const mins = timeToMinutes(s.time);
+ return mins >= timeToMinutes('08:00') && mins <= timeToMinutes('16:30');
+ });
container.innerHTML = `
@@ -2222,12 +2411,20 @@
Ledige tider
+
+
+ Anbefalede tider passer bedst i vores kalender
+
- ${times.map(t => {
+ ${displaySlots.map(slot => {
let cls = 'time-slot';
- if (taken.includes(t)) cls += ' disabled';
- if (state.time === t) cls += ' selected';
- return `
${t}
`;
+ if (slot.taken) cls += ' disabled';
+ if (slot.recommended && !slot.taken) cls += ' recommended';
+ if (state.time === slot.time) cls += ' selected';
+ return `
+ ${slot.time}
+ ${slot.recommended && !slot.taken ? '' : ''}
+
`;
}).join('')}
diff --git a/wwwroot/poc-booking.html b/wwwroot/poc-booking.html
index 9d5fa4e..4de81c9 100644
--- a/wwwroot/poc-booking.html
+++ b/wwwroot/poc-booking.html
@@ -568,6 +568,59 @@
text-decoration: line-through;
}
+ swp-time-slot.recommended {
+ position: relative;
+ border: 2px solid var(--color-green);
+ background: color-mix(in srgb, var(--color-green) 10%, white);
+ }
+
+ swp-time-slot.recommended:hover {
+ background: color-mix(in srgb, var(--color-green) 18%, white);
+ }
+
+ swp-time-slot.recommended.selected {
+ background: var(--color-teal);
+ border-color: var(--color-teal);
+ }
+
+ swp-ai-badge {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ background: var(--color-green);
+ color: white;
+ font-size: 9px;
+ font-weight: 600;
+ padding: 3px 6px;
+ border-radius: 4px;
+ white-space: nowrap;
+ }
+
+ swp-ai-badge i {
+ font-size: 10px;
+ }
+
+ swp-ai-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 14px;
+ background: color-mix(in srgb, var(--color-green) 10%, white);
+ border: 1px solid color-mix(in srgb, var(--color-green) 25%, transparent);
+ border-radius: 8px;
+ margin-bottom: 16px;
+ font-size: 12px;
+ color: var(--color-text);
+ }
+
+ swp-ai-info i {
+ font-size: 18px;
+ color: var(--color-green);
+ }
+
/* ==========================================
CONTACT FORM
========================================== */
@@ -1065,6 +1118,180 @@
{ id: "EMP004", name: "Viktor", role: "Frisør", color: "#009688" }
];
+ // ==========================================
+ // EKSISTERENDE BOOKINGER (Mock data til AI-optimering)
+ // ==========================================
+ const existingBookings = {
+ // Bookinger pr. medarbejder pr. dato
+ 'EMP001': {
+ '2026-01-06': [
+ { start: '10:00', end: '11:00', service: 'Dameklip' },
+ { start: '13:30', end: '14:30', service: 'Herreklip' }
+ ],
+ '2026-01-07': [
+ { start: '09:00', end: '10:30', service: 'Bundfarve' },
+ { start: '11:00', end: '12:00', service: 'Dameklip' },
+ { start: '14:00', end: '15:00', service: 'Dameklip' }
+ ]
+ },
+ 'EMP002': {
+ '2026-01-06': [
+ { start: '09:00', end: '10:00', service: 'Herreklip' },
+ { start: '11:00', end: '12:00', service: 'Dameklip' },
+ { start: '15:00', end: '16:30', service: 'Striber' }
+ ],
+ '2026-01-07': [
+ { start: '10:00', end: '11:00', service: 'Dameklip' },
+ { start: '13:00', end: '14:00', service: 'Herreklip' }
+ ]
+ },
+ 'EMP003': {
+ '2026-01-06': [
+ { start: '08:30', end: '09:30', service: 'Herreklip' },
+ { start: '12:00', end: '13:00', service: 'Dameklip' }
+ ]
+ },
+ 'EMP004': {
+ '2026-01-06': [
+ { start: '09:00', end: '10:00', service: 'Herreklip' },
+ { start: '14:00', end: '15:30', service: 'Bundfarve' }
+ ]
+ }
+ };
+
+ const SALON_OPEN = '08:00';
+ const SALON_CLOSE = '17:00';
+
+ // ==========================================
+ // AI SLOT OPTIMERING
+ // ==========================================
+ function timeToMinutes(time) {
+ const [h, m] = time.split(':').map(Number);
+ return h * 60 + m;
+ }
+
+ function minutesToTime(mins) {
+ const h = Math.floor(mins / 60);
+ const m = mins % 60;
+ return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
+ }
+
+ function getBookingsForSlot(date, employeeId) {
+ // Hvis ingen præference, kombiner alle medarbejderes bookinger
+ if (!employeeId) {
+ // Find medarbejder med færrest bookinger (simulerer "første ledige")
+ let bestEmployee = 'EMP001';
+ let minBookings = Infinity;
+ for (const empId of Object.keys(existingBookings)) {
+ const bookings = existingBookings[empId]?.[date] || [];
+ if (bookings.length < minBookings) {
+ minBookings = bookings.length;
+ bestEmployee = empId;
+ }
+ }
+ return existingBookings[bestEmployee]?.[date] || [];
+ }
+ return existingBookings[employeeId]?.[date] || [];
+ }
+
+ function calculateOptimalSlots(serviceDuration, date, employeeId) {
+ const bookings = getBookingsForSlot(date, employeeId);
+ const openMins = timeToMinutes(SALON_OPEN);
+ const closeMins = timeToMinutes(SALON_CLOSE);
+
+ // Generer alle mulige slots (30 min intervaller)
+ const slots = [];
+ for (let mins = openMins; mins <= closeMins - serviceDuration; mins += 30) {
+ const slotStart = mins;
+ const slotEnd = mins + serviceDuration;
+ const time = minutesToTime(mins);
+
+ // Tjek om slot overlapper med eksisterende booking
+ let isTaken = false;
+ for (const booking of bookings) {
+ const bookStart = timeToMinutes(booking.start);
+ const bookEnd = timeToMinutes(booking.end);
+ if (slotStart < bookEnd && slotEnd > bookStart) {
+ isTaken = true;
+ break;
+ }
+ }
+
+ // Beregn score for dette slot
+ let score = 0;
+
+ if (!isTaken) {
+ // +3: Starter ved åbningstid
+ if (slotStart === openMins) {
+ score += 3;
+ }
+
+ // +3: Slutter præcis på næste booking
+ for (const booking of bookings) {
+ const bookStart = timeToMinutes(booking.start);
+ if (slotEnd === bookStart) {
+ score += 3;
+ break;
+ }
+ }
+
+ // +2: Starter lige efter en booking
+ for (const booking of bookings) {
+ const bookEnd = timeToMinutes(booking.end);
+ if (slotStart === bookEnd) {
+ score += 2;
+ break;
+ }
+ }
+
+ // -2: Skaber lille hul (< 30 min) til næste booking
+ for (const booking of bookings) {
+ const bookStart = timeToMinutes(booking.start);
+ const gap = bookStart - slotEnd;
+ if (gap > 0 && gap < 30) {
+ score -= 2;
+ break;
+ }
+ }
+
+ // -2: Skaber lille hul fra forrige booking
+ for (const booking of bookings) {
+ const bookEnd = timeToMinutes(booking.end);
+ const gap = slotStart - bookEnd;
+ if (gap > 0 && gap < 30) {
+ score -= 2;
+ break;
+ }
+ }
+
+ // +1: Sidst på dagen (fylder op bagfra)
+ if (slotEnd >= closeMins - 60) {
+ score += 1;
+ }
+ }
+
+ slots.push({
+ time,
+ taken: isTaken,
+ score,
+ recommended: false
+ });
+ }
+
+ // Marker top 3 ledige slots som recommended
+ const availableSlots = slots.filter(s => !s.taken);
+ availableSlots.sort((a, b) => b.score - a.score);
+
+ const topSlots = availableSlots.slice(0, 3);
+ for (const slot of topSlots) {
+ if (slot.score > 0) {
+ slot.recommended = true;
+ }
+ }
+
+ return slots;
+ }
+
// ==========================================
// INIT
// ==========================================
@@ -1294,6 +1521,10 @@
Ledige tider
+
+
+ Anbefalede tider passer bedst i vores kalender og minimerer ventetid
+
@@ -1356,11 +1587,22 @@
const grid = document.getElementById('timeGrid');
if (!grid) return;
- const times = ['09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30'];
- const taken = ['10:30', '14:00'];
+ // Beregn samlet varighed af valgte services (eller default 60 min)
+ const serviceDuration = state.services.reduce((sum, s) => sum + s.duration, 0) || 60;
- grid.innerHTML = times.map(t => `
-
${t}
+ // Brug AI-algoritmen til at beregne optimale slots
+ const selectedDate = state.date || new Date().toISOString().split('T')[0];
+ const slots = calculateOptimalSlots(serviceDuration, selectedDate, state.employee);
+
+ // Render slots med AI-anbefalinger
+ grid.innerHTML = slots.map(slot => `
+
+ ${slot.time}
+ ${slot.recommended ? 'Anbefalet' : ''}
+
`).join('');
grid.querySelectorAll('swp-time-slot:not(.disabled)').forEach(slot => {
diff --git a/wwwroot/poc-employee.html b/wwwroot/poc-employee.html
index c2e3ad0..4c5e5f6 100644
--- a/wwwroot/poc-employee.html
+++ b/wwwroot/poc-employee.html
@@ -2609,21 +2609,17 @@
Provision
+
+ Minimum pr. time
+ 220 kr
+
På services
- 12%
+ 15%
På produktsalg
- 8%
-
-
- Bonus ved mål
- 2.500 kr/md
-
-
- Månedligt mål
- 45.000 kr
+ 15%
diff --git a/wwwroot/poc-indstillinger.html b/wwwroot/poc-indstillinger.html
index 5b9697e..a2bbfa0 100644
--- a/wwwroot/poc-indstillinger.html
+++ b/wwwroot/poc-indstillinger.html
@@ -745,6 +745,94 @@
margin: 20px 0;
}
+ /* ==========================================
+ CLOSED DAYS LIST
+ ========================================== */
+ swp-closed-days-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ }
+
+ swp-closed-day-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ swp-closed-day-item:last-child {
+ border-bottom: none;
+ }
+
+ swp-closed-day-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ flex: 1;
+ }
+
+ swp-closed-day-date {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-text);
+ }
+
+ swp-closed-day-name {
+ font-size: 12px;
+ color: var(--color-text-secondary);
+ }
+
+ swp-closed-day-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 4px 10px;
+ font-size: 11px;
+ font-weight: 500;
+ border-radius: 4px;
+ flex-shrink: 0;
+ }
+
+ swp-closed-day-badge.holiday {
+ background: color-mix(in srgb, var(--color-purple) 15%, transparent);
+ color: var(--color-purple);
+ }
+
+ swp-closed-day-badge.custom {
+ background: color-mix(in srgb, var(--color-amber) 15%, transparent);
+ color: var(--color-amber);
+ }
+
+ swp-closed-day-item swp-icon-btn {
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ background: transparent;
+ border-radius: 6px;
+ cursor: pointer;
+ color: var(--color-text-secondary);
+ transition: all 150ms ease;
+ flex-shrink: 0;
+ }
+
+ swp-closed-day-item swp-icon-btn i {
+ font-size: 16px;
+ }
+
+ swp-closed-day-item swp-icon-btn.edit:hover {
+ background: color-mix(in srgb, var(--color-teal) 15%, transparent);
+ color: var(--color-teal);
+ }
+
+ swp-closed-day-item swp-icon-btn.delete:hover {
+ background: color-mix(in srgb, var(--color-red) 15%, transparent);
+ color: var(--color-red);
+ }
+
/* ==========================================
TWO COLUMN GRID
========================================== */
@@ -1315,6 +1403,283 @@
background: rgba(0, 137, 123, 0.35);
box-shadow: 0 0 0 2px rgba(0, 137, 123, 0.2);
}
+
+ /* ==========================================
+ MODULES TAB STYLES
+ ========================================== */
+ swp-modules-section {
+ display: block;
+ margin-bottom: 32px;
+ }
+
+ swp-modules-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 16px;
+ }
+
+ swp-modules-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--color-text);
+ }
+
+ swp-modules-title i {
+ font-size: 20px;
+ color: var(--color-teal);
+ }
+
+ swp-modules-badge {
+ display: inline-flex;
+ align-items: center;
+ padding: 4px 10px;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ border-radius: 20px;
+ background: color-mix(in srgb, var(--color-purple) 15%, transparent);
+ color: var(--color-purple);
+ }
+
+ swp-modules-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 16px;
+ }
+
+ @media (max-width: 768px) {
+ swp-modules-grid {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ swp-module-card {
+ display: block;
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: 10px;
+ overflow: hidden;
+ transition: all 150ms ease;
+ }
+
+ swp-module-card:hover {
+ border-color: var(--color-teal);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+ }
+
+ swp-module-header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 20px;
+ }
+
+ swp-module-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ swp-module-icon i {
+ font-size: 24px;
+ }
+
+ swp-module-icon.teal {
+ background: color-mix(in srgb, var(--color-teal) 15%, transparent);
+ color: var(--color-teal);
+ }
+
+ swp-module-icon.purple {
+ background: color-mix(in srgb, var(--color-purple) 15%, transparent);
+ color: var(--color-purple);
+ }
+
+ swp-module-icon.blue {
+ background: color-mix(in srgb, var(--color-blue) 15%, transparent);
+ color: var(--color-blue);
+ }
+
+ swp-module-icon.amber {
+ background: color-mix(in srgb, var(--color-amber) 15%, transparent);
+ color: var(--color-amber);
+ }
+
+ swp-module-icon.green {
+ background: color-mix(in srgb, var(--color-green) 15%, transparent);
+ color: var(--color-green);
+ }
+
+ swp-module-info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ swp-module-title {
+ display: block;
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--color-text);
+ margin-bottom: 4px;
+ }
+
+ swp-module-desc {
+ display: block;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+ line-height: 1.4;
+ }
+
+ swp-module-toggle {
+ flex-shrink: 0;
+ }
+
+ swp-module-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 20px;
+ background: var(--color-background-alt);
+ border-top: 1px solid var(--color-border);
+ }
+
+ swp-module-tags {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ swp-module-tag {
+ display: inline-flex;
+ align-items: center;
+ padding: 4px 10px;
+ font-size: 12px;
+ font-weight: 500;
+ border-radius: 4px;
+ background: var(--color-background);
+ color: var(--color-text-secondary);
+ }
+
+ swp-module-tag.included {
+ background: color-mix(in srgb, var(--color-green) 15%, transparent);
+ color: var(--color-green);
+ }
+
+ swp-module-tag.price {
+ background: color-mix(in srgb, var(--color-blue) 15%, transparent);
+ color: var(--color-blue);
+ }
+
+ swp-module-tag.new {
+ background: color-mix(in srgb, var(--color-purple) 15%, transparent);
+ color: var(--color-purple);
+ }
+
+ swp-module-tag.coming {
+ background: color-mix(in srgb, var(--color-amber) 15%, transparent);
+ color: var(--color-amber);
+ }
+
+ /* Featured module card */
+ swp-module-card.featured {
+ border: 2px solid var(--color-green);
+ background: linear-gradient(135deg, color-mix(in srgb, var(--color-green) 3%, var(--color-surface)) 0%, var(--color-surface) 100%);
+ }
+
+ swp-module-card.featured:hover {
+ border-color: var(--color-green);
+ box-shadow: 0 4px 16px rgba(67, 160, 71, 0.15);
+ }
+
+ swp-module-features {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 16px;
+ padding: 0 20px 16px;
+ }
+
+ swp-module-feature {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 13px;
+ color: var(--color-text-secondary);
+ }
+
+ swp-module-feature i {
+ font-size: 16px;
+ color: var(--color-green);
+ }
+
+ swp-module-stats {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+ padding: 16px 20px;
+ background: color-mix(in srgb, var(--color-green) 6%, transparent);
+ border-top: 1px solid color-mix(in srgb, var(--color-green) 20%, transparent);
+ border-bottom: 1px solid color-mix(in srgb, var(--color-green) 20%, transparent);
+ }
+
+ swp-module-stat {
+ text-align: center;
+ }
+
+ swp-module-stat-value {
+ display: block;
+ font-size: 20px;
+ font-weight: 700;
+ font-family: var(--font-mono);
+ color: var(--color-green);
+ }
+
+ swp-module-stat-label {
+ display: block;
+ font-size: 11px;
+ color: var(--color-text-secondary);
+ margin-top: 2px;
+ }
+
+ /* Purple featured variant */
+ swp-module-card.featured.purple {
+ border-color: var(--color-purple);
+ background: linear-gradient(135deg, color-mix(in srgb, var(--color-purple) 3%, var(--color-surface)) 0%, var(--color-surface) 100%);
+ }
+
+ swp-module-card.featured.purple:hover {
+ border-color: var(--color-purple);
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.15);
+ }
+
+ swp-module-feature.purple i {
+ color: var(--color-purple);
+ }
+
+ swp-module-stats.purple {
+ background: color-mix(in srgb, var(--color-purple) 6%, transparent);
+ border-top-color: color-mix(in srgb, var(--color-purple) 20%, transparent);
+ border-bottom-color: color-mix(in srgb, var(--color-purple) 20%, transparent);
+ }
+
+ swp-module-stats.purple swp-module-stat-value {
+ color: var(--color-purple);
+ }
+
+ swp-btn.primary.purple {
+ background: var(--color-purple);
+ }
+
+ swp-btn.primary.purple:hover {
+ background: color-mix(in srgb, var(--color-purple) 85%, black);
+ }
@@ -1362,6 +1727,10 @@
Betalinger
+
+
+ Moduler
+
+
+
+
+
+
+
+ Løn & Økonomi
+
+
+
+
+
+
+
+
+
+
+
+ Lønberegning
+ Beregn løn, overtid, provision og ferie automatisk. Grundmodul for løneksport til eksterne systemer.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+
+ Indstillinger
+
+
+
+
+
+
+
+
+
+
+ Intect
+ Eksporter direkte til Intect lønsystem i StandardMapping-format.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+
+ Indstillinger
+
+
+
+
+
+
+
+
+
+
+ Proløn
+ Eksporter direkte til Proløn lønsystem.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Kommer
+
+
+
+
+
+
+
+
+
+
+
+ Danløn
+ Eksporter direkte til Danløn lønsystem.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Kommer
+
+
+
+
+
+
+
+
+
+
+
+ Salary.dk
+ Eksporter direkte til Salary.dk lønsystem.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Kommer
+
+
+
+
+
+
+
+
+
+
+
+ Zenegy
+ Eksporter direkte til Zenegy lønsystem. Automatisk overførsel af timer og provision.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Kommer
+
+
+
+
+
+
+
+
+
+
+
+ AI & Analyse
+
+ Nyt
+
+
+
+
+
+
+
+
+
+
+ AI Dashboard
+ Din personlige AI-assistent på dashboardet. Få daglige indsigter, anbefalinger og svar på spørgsmål om din forretning.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+
+ Indstillinger
+
+
+
+
+
+
+
+
+
+
+ AI Virksomhedsanalyse
+ Dybdegående AI-analyse af timer vs. omsætning, belægningsgrad og identificer mønstre og vækstmuligheder.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ +49 kr/md
+ Beta
+
+ Læs mere
+
+
+
+
+
+
+
+
+
+
+ AI Produktsalg
+ Øg dit produktsalg med intelligente anbefalinger. AI'en lærer dine kunders præferencer og foreslår de rigtige produkter på det rigtige tidspunkt.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+
+ Personlige produktanbefalinger
+
+
+
+ Automatisk mersalg ved kassen
+
+
+
+ Kundeprofilanalyse
+
+
+
+ Sæson- og trendbaserede forslag
+
+
+
+
+ +23%
+ Mersalg pr. kunde
+
+
+ 4.2x
+ Højere konvertering
+
+
+ 89%
+ Relevante forslag
+
+
+
+
+ +49 kr/md
+ Beta
+
+ Prøv gratis i 14 dage
+
+
+
+
+
+
+
+
+
+
+ AI Kalenderoptimering
+ Maksimer din kalenderudnyttelse og reducer tabt omsætning. AI'en foreslår optimale tider til kunder og identificerer huller der kan fyldes.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+
+ Smart tidsforslag ved booking
+
+
+
+ Automatisk hul-identifikation
+
+
+
+ SMS-tilbud til flytning af tider
+
+
+
+ Dashboard med optimeringsscore
+
+
+
+
+ 52x
+ ROI i gennemsnit
+
+
+ 15%
+ Færre tomme slots
+
+
+ 312k
+ Ekstra oms./år*
+
+
+
+
+ +99 kr/md
+ Ny
+
+ Prøv gratis i 14 dage
+
+
+
+
+
+
+
+
+
+
+ Tillægsmoduler
+
+
+
+
+
+
+
+
+
+
+
+ Online Booking
+ Lad kunder booke tider online via din egen bookingside. Integreres med kalender og påmindelser.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+
+ Indstillinger
+
+
+
+
+
+
+
+
+
+
+ Gavekort
+ Sælg og administrer digitale gavekort. Kunderne kan købe online eller i butikken, og indløse ved betaling.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+
+ Indstillinger
+
+
+
+
+
+
+
+
+
+
+ Kasseafstemning
+ Daglig kasseopgørelse og afstemning. Hold styr på kontanter, kort og andre betalingsmetoder.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+
+ Indstillinger
+
+
+
+
+
+
+
+
+
+
+ Leverandører
+ Bestil varer direkte fra dine leverandører. Hold styr på ordrer, leveringer og lagerbeholdning.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+
+ Indstillinger
+
+
+
+
+
+
+
+
+
+
+ Stregkodescanner
+ Scan EAN-koder og få AI-genererede produktbeskrivelser automatisk. Opret nye produkter på sekunder.
+
+
+
+ Til
+ Fra
+
+
+
+
+
+ Inkluderet
+ AI
+
+ Indstillinger
+
+
+
+
+
+
diff --git a/wwwroot/poc-loen-provision.html b/wwwroot/poc-loen-provision.html
new file mode 100644
index 0000000..228091d
--- /dev/null
+++ b/wwwroot/poc-loen-provision.html
@@ -0,0 +1,1525 @@
+
+
+
+
+
+
Løn & Provision
+
+
+
+
+
+
+
+
+
+
+
+ Tilbage
+
+ Løn & Provision
+
+
+
+
+ Eksporter
+
+
+
+
+
+
+
+
+ Vælg periode
+
+
+
+ År:
+
+
+
+ Fra uge:
+
+
+ —
+
+ Til uge:
+
+
+
+
+ Hent data
+
+
+
+
+
+
+
+
+ Samlet overblik for perioden
+
+
+
+ 0 kr
+ Total løn
+
+
+ 0 kr
+ Grundløn
+
+
+ 0 kr
+ Overtidstillæg
+
+
+ 0 kr
+ Prov. services
+
+
+ 0 kr
+ Prov. produkter
+
+
+
+
+
+
+
+
+
+
+
+ Løneksport til Intect
+
+
+
+
+
+
+
+
+ Kopier alle til clipboard
+
+
+
+ Eksporter til Intect
+
+
+
+
+
+
+
+