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 + + + + + + + +