Calendar/wwwroot/poc-produkt-opret.html
Janus C. H. Knudsen 3b86a6c8b3 Adds debug codes for product scanning
Includes example EAN codes for testing product scanning functionality
Enhances UI with debug section for easier testing of scanner feature

Improves development and testing experience
2026-01-02 09:30:38 +01:00

1458 lines
41 KiB
HTML

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Opret nyt produkt</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ==========================================
FONT FACE (Poppins)
========================================== */
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* ==========================================
CSS VARIABLES (Design System)
========================================== */
:root {
--color-surface: #fff;
--color-background: #f5f5f5;
--color-background-hover: #f0f0f0;
--color-background-alt: #fafafa;
--color-border: #e0e0e0;
--color-text: #333;
--color-text-secondary: #666;
--color-teal: #00897b;
--color-blue: #1976d2;
--color-red: #e53935;
--color-amber: #f59e0b;
--color-purple: #8b5cf6;
--color-green: #43a047;
--font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
}
/* ==========================================
RESET & BASE
========================================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
font-size: 14px;
color: var(--color-text);
background: var(--color-background);
line-height: 1.5;
}
a {
color: var(--color-teal);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* ==========================================
TOPBAR
========================================== */
swp-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 100;
}
swp-topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
swp-back-link {
display: flex;
align-items: center;
gap: 6px;
color: var(--color-text-secondary);
text-decoration: none;
font-size: 13px;
cursor: pointer;
transition: color 150ms ease;
}
swp-back-link:hover {
color: var(--color-teal);
}
swp-back-link svg {
width: 16px;
height: 16px;
fill: currentColor;
}
swp-page-title {
font-size: 16px;
font-weight: 600;
}
swp-topbar-right {
display: flex;
align-items: center;
gap: 12px;
}
/* ==========================================
LAYOUT
========================================== */
swp-page-container {
display: block;
max-width: 1200px;
margin: 0 auto;
padding: 24px;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 900px) {
.grid-2 { grid-template-columns: 1fr; }
}
/* ==========================================
CARDS
========================================== */
swp-card {
display: block;
background: var(--color-surface);
border-radius: 8px;
border: 1px solid var(--color-border);
margin-bottom: 20px;
overflow: hidden;
}
swp-card-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid var(--color-border);
}
swp-card-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
}
swp-card-hint {
font-size: 12px;
color: var(--color-text-secondary);
}
swp-card-body {
display: block;
padding: 20px;
}
/* ==========================================
BUTTONS
========================================== */
swp-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
font-size: 13px;
font-weight: 500;
font-family: var(--font-family);
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
border: none;
}
swp-btn svg {
width: 16px;
height: 16px;
fill: currentColor;
}
swp-btn.primary {
background: var(--color-teal);
color: white;
}
swp-btn.primary:hover {
background: #00796b;
}
swp-btn.secondary {
background: var(--color-surface);
border: 1px solid var(--color-border);
color: var(--color-text);
}
swp-btn.secondary:hover {
background: var(--color-background-hover);
}
swp-btn.small {
padding: 6px 12px;
font-size: 12px;
}
/* ==========================================
FORM ELEMENTS
========================================== */
swp-form-grid {
display: grid;
gap: 16px;
}
swp-form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
swp-form-field {
display: flex;
flex-direction: column;
gap: 6px;
}
swp-form-field.full {
grid-column: 1 / -1;
}
swp-form-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
}
swp-form-field input,
swp-form-field select,
swp-form-field textarea {
padding: 10px 12px;
font-size: 14px;
font-family: var(--font-family);
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-surface);
color: var(--color-text);
}
swp-form-field input:focus,
swp-form-field select:focus,
swp-form-field textarea:focus {
outline: none;
border-color: var(--color-teal);
}
swp-form-field textarea {
min-height: 150px;
resize: vertical;
}
swp-form-field input.mono {
font-family: var(--font-mono);
}
swp-form-field input[readonly] {
background: var(--color-background-alt);
color: var(--color-text-secondary);
}
/* ==========================================
TOGGLE SLIDER (Ja/Nej)
========================================== */
swp-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
}
swp-toggle-label {
font-size: 14px;
font-weight: 500;
}
swp-toggle-slider {
display: inline-flex;
width: fit-content;
background: var(--color-background);
border-radius: 6px;
border: 1px solid var(--color-border);
position: relative;
cursor: pointer;
}
swp-toggle-slider::before {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: calc(50% - 4px);
height: calc(100% - 4px);
background: color-mix(in srgb, var(--color-green) 18%, white);
border-radius: 4px;
transition: transform 200ms ease, background 200ms ease;
}
swp-toggle-slider[data-value="no"]::before {
transform: translateX(100%);
background: color-mix(in srgb, var(--color-red) 18%, white);
}
swp-toggle-option {
position: relative;
z-index: 1;
padding: 5px 16px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
transition: color 200ms ease;
user-select: none;
}
swp-toggle-slider[data-value="yes"] swp-toggle-option:first-child {
color: var(--color-green);
font-weight: 600;
}
swp-toggle-slider[data-value="no"] swp-toggle-option:last-child {
color: var(--color-red);
font-weight: 600;
}
/* ==========================================
VARIANTS TABLE
========================================== */
swp-variants-table {
display: block;
width: 100%;
}
swp-variants-header,
swp-variants-row {
display: grid;
grid-template-columns: 1fr 120px 80px 100px 80px;
align-items: center;
gap: 12px;
}
swp-variants-header {
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
}
swp-variants-header span {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
}
swp-variants-header span.right {
text-align: right;
}
swp-variants-row {
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
swp-variants-row:last-child {
border-bottom: none;
}
swp-variants-row span {
font-size: 14px;
}
swp-variants-row span.mono {
font-family: var(--font-mono);
font-size: 13px;
}
swp-variants-row span.right {
text-align: right;
}
swp-variants-row span.muted {
color: var(--color-text-secondary);
}
swp-variants-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
swp-variants-actions button {
width: 28px;
height: 28px;
border: none;
background: var(--color-background);
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 150ms ease;
}
swp-variants-actions button:hover {
background: var(--color-border);
}
swp-variants-actions button svg {
width: 14px;
height: 14px;
fill: var(--color-text-secondary);
}
swp-add-variant {
display: flex;
justify-content: center;
padding-top: 16px;
}
/* ==========================================
PRICE & MARGIN
========================================== */
swp-price-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
swp-margin-display {
display: flex;
gap: 24px;
padding: 16px;
background: var(--color-background-alt);
border-radius: 6px;
margin-top: 16px;
}
swp-margin-item {
display: flex;
flex-direction: column;
gap: 4px;
}
swp-margin-label {
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary);
}
swp-margin-value {
font-size: 20px;
font-weight: 600;
font-family: var(--font-mono);
color: var(--color-green);
}
/* ==========================================
STOCK DISPLAY
========================================== */
swp-stock-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
/* ==========================================
SUPPLIER INFO
========================================== */
swp-supplier-grid {
display: grid;
gap: 16px;
}
/* ==========================================
BARCODE SCANNER
========================================== */
swp-scanner-card {
display: block;
background: var(--color-surface);
border-radius: 8px;
border: 2px solid var(--color-border);
margin-bottom: 20px;
overflow: hidden;
transition: border-color 300ms ease;
}
swp-scanner-card.scanning {
border-color: var(--color-blue);
}
swp-scanner-card.success {
border-color: var(--color-green);
}
swp-scanner-card.error {
border-color: var(--color-amber);
}
swp-scanner-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 20px;
border-bottom: 1px solid var(--color-border);
background: var(--color-background-alt);
}
swp-scanner-header svg {
width: 20px;
height: 20px;
fill: var(--color-teal);
}
swp-scanner-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
}
swp-scanner-body {
padding: 24px;
}
/* Scanner States */
swp-scanner-ready {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
}
swp-scan-button {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
padding: 20px 32px;
font-size: 16px;
font-weight: 600;
font-family: var(--font-family);
color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 8%, white);
border: 2px dashed var(--color-teal);
border-radius: 12px;
cursor: pointer;
transition: all 200ms ease;
}
swp-scan-button:hover {
background: color-mix(in srgb, var(--color-teal) 15%, white);
border-style: solid;
}
swp-scan-button svg {
width: 28px;
height: 28px;
fill: currentColor;
}
swp-scanner-hint {
font-size: 12px;
color: var(--color-text-secondary);
text-align: center;
}
/* Scanning State */
swp-scanner-scanning {
display: none;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
}
swp-scanner-scanning.active {
display: flex;
}
swp-scanning-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: var(--color-blue);
font-weight: 500;
}
swp-scanning-status .pulse {
width: 10px;
height: 10px;
background: var(--color-blue);
border-radius: 50%;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
swp-scanned-code {
font-size: 36px;
font-weight: 700;
font-family: var(--font-mono);
letter-spacing: 4px;
color: var(--color-text);
min-height: 48px;
}
.scanner-input {
position: absolute;
left: -9999px;
opacity: 0;
pointer-events: none;
}
swp-debug-codes {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 16px;
padding: 12px;
background: var(--color-background-alt);
border: 1px dashed var(--color-border);
border-radius: 6px;
font-size: 11px;
color: var(--color-text-secondary);
}
swp-debug-codes strong {
color: var(--color-text);
margin-bottom: 4px;
}
swp-debug-codes code {
font-family: var(--font-mono);
font-size: 11px;
background: var(--color-surface);
padding: 2px 6px;
border-radius: 3px;
margin-right: 8px;
user-select: all;
cursor: pointer;
}
/* Loading State */
swp-scanner-loading {
display: none;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
}
swp-scanner-loading.active {
display: flex;
}
swp-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border);
border-top-color: var(--color-teal);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
swp-loading-text {
font-size: 14px;
color: var(--color-text-secondary);
}
swp-loading-code {
font-size: 20px;
font-weight: 600;
font-family: var(--font-mono);
letter-spacing: 2px;
color: var(--color-text);
}
/* Success State */
swp-scanner-success {
display: none;
flex-direction: column;
gap: 20px;
padding: 20px;
}
swp-scanner-success.active {
display: flex;
}
swp-success-header {
display: flex;
align-items: center;
gap: 10px;
color: var(--color-green);
font-weight: 600;
font-size: 15px;
}
swp-success-header svg {
width: 22px;
height: 22px;
fill: currentColor;
}
swp-product-preview {
background: var(--color-background-alt);
border-radius: 8px;
padding: 16px;
}
swp-preview-row {
display: flex;
padding: 8px 0;
border-bottom: 1px solid var(--color-border);
}
swp-preview-row:last-child {
border-bottom: none;
}
swp-preview-label {
width: 100px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
flex-shrink: 0;
}
swp-preview-value {
font-size: 14px;
color: var(--color-text);
}
swp-preview-value.description {
font-size: 13px;
line-height: 1.5;
}
swp-scanner-actions {
display: flex;
flex-direction: column;
gap: 10px;
}
swp-scanner-actions swp-btn {
width: 100%;
justify-content: center;
}
swp-btn.success {
background: var(--color-green);
color: white;
}
swp-btn.success:hover {
background: #388e3c;
}
/* Error/Not Found State */
swp-scanner-notfound {
display: none;
flex-direction: column;
gap: 20px;
padding: 20px;
}
swp-scanner-notfound.active {
display: flex;
}
swp-notfound-header {
display: flex;
align-items: center;
gap: 10px;
color: var(--color-amber);
font-weight: 600;
font-size: 15px;
}
swp-notfound-header svg {
width: 22px;
height: 22px;
fill: currentColor;
}
swp-notfound-code {
font-size: 24px;
font-weight: 600;
font-family: var(--font-mono);
letter-spacing: 2px;
color: var(--color-text);
text-align: center;
padding: 16px;
background: var(--color-background-alt);
border-radius: 8px;
}
swp-notfound-message {
font-size: 14px;
color: var(--color-text-secondary);
text-align: center;
line-height: 1.6;
}
</style>
</head>
<body>
<!-- Topbar -->
<swp-topbar>
<swp-topbar-left>
<swp-back-link>
<svg viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
Produkter
</swp-back-link>
<swp-page-title>Nyt produkt</swp-page-title>
</swp-topbar-left>
<swp-topbar-right>
<swp-btn class="secondary">Annuller</swp-btn>
<swp-btn class="primary">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
Opret produkt
</swp-btn>
</swp-topbar-right>
</swp-topbar>
<swp-page-container>
<div class="grid-2">
<!-- VENSTRE KOLONNE -->
<div>
<!-- Basisoplysninger -->
<swp-card>
<swp-card-header>
<swp-card-title>Basisoplysninger</swp-card-title>
</swp-card-header>
<swp-card-body>
<swp-form-grid>
<swp-form-field class="full">
<swp-form-label>Produktnavn</swp-form-label>
<input type="text" id="productName" placeholder="Indtast produktnavn..." />
</swp-form-field>
<swp-form-row>
<swp-form-field>
<swp-form-label>Varenummer</swp-form-label>
<input type="text" id="productSku" class="mono" placeholder="F.eks. ABC-123" />
</swp-form-field>
<swp-form-field>
<swp-form-label>EAN / Stregkode</swp-form-label>
<input type="text" id="productEan" class="mono" placeholder="Scannes automatisk" />
</swp-form-field>
</swp-form-row>
<swp-form-row>
<swp-form-field>
<swp-form-label>Kategori</swp-form-label>
<select id="productCategory">
<option value="">Vælg kategori...</option>
<option value="haarpleje">Hårpleje</option>
<option value="styling">Styling</option>
<option value="farve">Farve</option>
<option value="tilbehoer">Tilbehør</option>
</select>
</swp-form-field>
<swp-form-field>
<swp-form-label>Brand</swp-form-label>
<select id="productBrand">
<option value="">Vælg brand...</option>
<option value="redken">Redken</option>
<option value="olaplex">Olaplex</option>
<option value="kerastase">Kérastase</option>
<option value="wella">Wella</option>
<option value="loreal">L'Oréal</option>
</select>
</swp-form-field>
</swp-form-row>
<swp-form-field class="full">
<swp-form-label>Beskrivelse</swp-form-label>
<textarea id="productDescription" placeholder="Indtast produktbeskrivelse..."></textarea>
</swp-form-field>
<swp-toggle-row>
<swp-toggle-label>Produkt aktiv</swp-toggle-label>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
</swp-toggle-row>
</swp-form-grid>
</swp-card-body>
</swp-card>
<!-- Leverandør -->
<swp-card>
<swp-card-header>
<swp-card-title>Leverandør</swp-card-title>
</swp-card-header>
<swp-card-body>
<swp-supplier-grid>
<swp-form-field>
<swp-form-label>Leverandør</swp-form-label>
<select>
<option value="">Vælg leverandør...</option>
<option>L'Oréal Professionnel</option>
<option>Beauty Group Denmark</option>
<option>Hairware ApS</option>
</select>
</swp-form-field>
<swp-form-row>
<swp-form-field>
<swp-form-label>Leverandør varenr</swp-form-label>
<input type="text" class="mono" placeholder="F.eks. E3845600" />
</swp-form-field>
<swp-form-field>
<swp-form-label>Leveringstid</swp-form-label>
<input type="text" placeholder="F.eks. 3-5 dage" />
</swp-form-field>
</swp-form-row>
</swp-supplier-grid>
</swp-card-body>
</swp-card>
<!-- Lager -->
<swp-card>
<swp-card-header>
<swp-card-title>Lagerindstillinger</swp-card-title>
</swp-card-header>
<swp-card-body>
<swp-stock-grid>
<swp-form-field>
<swp-form-label>Startlager</swp-form-label>
<input type="number" value="0" min="0" />
</swp-form-field>
<swp-form-field>
<swp-form-label>Minimumslager</swp-form-label>
<input type="number" value="5" min="0" />
</swp-form-field>
<swp-form-field>
<swp-form-label>Genbestillingspunkt</swp-form-label>
<input type="number" value="10" min="0" />
</swp-form-field>
</swp-stock-grid>
</swp-card-body>
</swp-card>
<!-- Varianter -->
<swp-card>
<swp-card-header>
<swp-card-title>Varianter</swp-card-title>
<swp-card-hint>Størrelser og variationer</swp-card-hint>
</swp-card-header>
<swp-card-body>
<swp-variants-table>
<swp-variants-header>
<span>Variant</span>
<span>Varenr</span>
<span class="right">Lager</span>
<span class="right">Pris</span>
<span></span>
</swp-variants-header>
<!-- Varianter tilføjes her -->
</swp-variants-table>
<swp-add-variant>
<swp-btn class="secondary small">+ Tilføj variant</swp-btn>
</swp-add-variant>
</swp-card-body>
</swp-card>
</div>
<!-- HØJRE KOLONNE -->
<div>
<!-- Stregkodescanner -->
<swp-scanner-card id="scannerCard">
<swp-scanner-header>
<svg viewBox="0 0 24 24"><path d="M2 6h2v12H2V6zm3 0h1v12H5V6zm2 0h3v12H7V6zm4 0h1v12h-1V6zm3 0h2v12h-2V6zm3 0h3v12h-3V6zm4 0h1v12h-1V6z"/></svg>
<swp-scanner-title>Stregkodescanner</swp-scanner-title>
</swp-scanner-header>
<swp-scanner-body>
<!-- Ready State (initial) -->
<swp-scanner-ready id="scannerReady">
<swp-scan-button id="scanButton">
<svg viewBox="0 0 24 24"><path d="M2 6h2v12H2V6zm3 0h1v12H5V6zm2 0h3v12H7V6zm4 0h1v12h-1V6zm3 0h2v12h-2V6zm3 0h3v12h-3V6zm4 0h1v12h-1V6z"/></svg>
SCAN STREGKODE
</swp-scan-button>
<swp-scanner-hint>Klik på knappen og scan produktets stregkode</swp-scanner-hint>
<swp-debug-codes>
<strong>Test EAN-koder:</strong>
<code>884486532879</code> Redken Serum
<code>3474636610143</code> Kérastase Olie
<code>0850018802239</code> Olaplex No.7
<code>4015600159122</code> Wella Glam Mist
</swp-debug-codes>
</swp-scanner-ready>
<!-- Scanning State -->
<swp-scanner-scanning id="scannerScanning">
<swp-scanning-status>
<span class="pulse"></span>
Klar til scanning...
</swp-scanning-status>
<swp-scanned-code id="scannedCodeDisplay"></swp-scanned-code>
<swp-scanner-hint>Scan nu produktets stregkode med din scanner</swp-scanner-hint>
</swp-scanner-scanning>
<!-- Loading State -->
<swp-scanner-loading id="scannerLoading">
<swp-loading-spinner></swp-loading-spinner>
<swp-loading-text>Henter produktinfo...</swp-loading-text>
<swp-loading-code id="loadingCode"></swp-loading-code>
</swp-scanner-loading>
<!-- Success State -->
<swp-scanner-success id="scannerSuccess">
<swp-success-header>
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
Produkt fundet!
</swp-success-header>
<swp-product-preview>
<swp-preview-row>
<swp-preview-label>Navn</swp-preview-label>
<swp-preview-value id="previewName"></swp-preview-value>
</swp-preview-row>
<swp-preview-row>
<swp-preview-label>Brand</swp-preview-label>
<swp-preview-value id="previewBrand"></swp-preview-value>
</swp-preview-row>
<swp-preview-row>
<swp-preview-label>Kategori</swp-preview-label>
<swp-preview-value id="previewCategory"></swp-preview-value>
</swp-preview-row>
<swp-preview-row>
<swp-preview-label>Beskrivelse</swp-preview-label>
<swp-preview-value id="previewDescription" class="description"></swp-preview-value>
</swp-preview-row>
</swp-product-preview>
<swp-scanner-actions>
<swp-btn class="success" id="useProductBtn">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
Brug disse oplysninger
</swp-btn>
<swp-btn class="secondary" id="scanAgainBtn">Scan igen</swp-btn>
</swp-scanner-actions>
</swp-scanner-success>
<!-- Not Found State -->
<swp-scanner-notfound id="scannerNotFound">
<swp-notfound-header>
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
Produktet blev ikke fundet
</swp-notfound-header>
<swp-notfound-code id="notFoundCode"></swp-notfound-code>
<swp-notfound-message>
EAN-koden er registreret i formularen.<br>
Udfyld resten af oplysningerne manuelt.
</swp-notfound-message>
<swp-scanner-actions>
<swp-btn class="secondary" id="scanAgainBtn2">Scan igen</swp-btn>
<swp-btn class="primary" id="fillManualBtn">Udfyld manuelt</swp-btn>
</swp-scanner-actions>
</swp-scanner-notfound>
<!-- Hidden scanner input -->
<input type="text" id="scannerInput" class="scanner-input" autocomplete="off" />
</swp-scanner-body>
</swp-scanner-card>
<!-- Pris & avance -->
<swp-card>
<swp-card-header>
<swp-card-title>Pris & avance</swp-card-title>
</swp-card-header>
<swp-card-body>
<swp-price-grid>
<swp-form-field>
<swp-form-label>Indkøbspris (ekskl. moms)</swp-form-label>
<input type="text" id="purchasePrice" class="mono" placeholder="0,00 kr" />
</swp-form-field>
<swp-form-field>
<swp-form-label>Salgspris (inkl. moms)</swp-form-label>
<input type="text" id="salePrice" class="mono" placeholder="0,00 kr" />
</swp-form-field>
</swp-price-grid>
<swp-margin-display>
<swp-margin-item>
<swp-margin-label>Avance</swp-margin-label>
<swp-margin-value id="marginAmount">0,00 kr</swp-margin-value>
</swp-margin-item>
<swp-margin-item>
<swp-margin-label>Avance %</swp-margin-label>
<swp-margin-value id="marginPercent">0%</swp-margin-value>
</swp-margin-item>
</swp-margin-display>
</swp-card-body>
</swp-card>
</div>
</div>
</swp-page-container>
<script>
// ==========================================
// MOCK PRODUCT DATABASE
// ==========================================
const mockProducts = {
'884486532879': {
name: 'Redken Acidic Bonding Concentrate 24/7 Night & Day Serum 100 ml',
brand: 'redken',
category: 'haarpleje',
description: 'Et plejende og styrkende reparationsserum, der arbejder kontinuerligt for at genopbygge svækket hårstruktur. Tilfører fugt og glans uden at tynge, og efterlader håret mere elastisk og sundt over tid. Særligt effektivt til skadet, farvebehandlet eller kemisk behandlet hår.\n\nFordel 1-2 pump i håndklædetørt eller tørt hår. Skal ikke skylles ud. Kan bruges dagligt og som natpleje for intensiv reparation.'
},
'3474636610143': {
name: 'Kérastase Elixir Ultime L\'Huile Originale 100 ml',
brand: 'kerastase',
category: 'haarpleje',
description: 'Luksus hårolie med argan olie og camellia olie. Giver fantastisk glans og blødhed uden at tynge håret. Perfekt til alle hårtyper som finishing produkt.'
},
'0850018802239': {
name: 'Olaplex No.7 Bonding Oil 30 ml',
brand: 'olaplex',
category: 'haarpleje',
description: 'Ultralettere, koncentreret stylingsolie, der dramatisk øger glans, blødhed og farveintensitet. Beskytter mod varme op til 230°C.'
},
'4015600159122': {
name: 'Wella Professionals EIMI Glam Mist 200 ml',
brand: 'wella',
category: 'styling',
description: 'Let glansspray der giver fantastisk glans og antistatisk effekt. Perfekt som finish på alle frisurer.'
}
};
// ==========================================
// SCANNER STATE MANAGEMENT
// ==========================================
const scannerCard = document.getElementById('scannerCard');
const scannerReady = document.getElementById('scannerReady');
const scannerScanning = document.getElementById('scannerScanning');
const scannerLoading = document.getElementById('scannerLoading');
const scannerSuccess = document.getElementById('scannerSuccess');
const scannerNotFound = document.getElementById('scannerNotFound');
const scanButton = document.getElementById('scanButton');
const scannerInput = document.getElementById('scannerInput');
const scannedCodeDisplay = document.getElementById('scannedCodeDisplay');
let currentState = 'ready';
let scannedCode = '';
let scanTimeout = null;
let foundProduct = null;
function setState(state) {
currentState = state;
// Hide all states
scannerReady.style.display = 'none';
scannerScanning.classList.remove('active');
scannerLoading.classList.remove('active');
scannerSuccess.classList.remove('active');
scannerNotFound.classList.remove('active');
// Reset card border
scannerCard.classList.remove('scanning', 'success', 'error');
switch(state) {
case 'ready':
scannerReady.style.display = 'flex';
break;
case 'scanning':
scannerScanning.classList.add('active');
scannerCard.classList.add('scanning');
break;
case 'loading':
scannerLoading.classList.add('active');
break;
case 'success':
scannerSuccess.classList.add('active');
scannerCard.classList.add('success');
break;
case 'notfound':
scannerNotFound.classList.add('active');
scannerCard.classList.add('error');
break;
}
}
// ==========================================
// SCANNER BUTTON CLICK
// ==========================================
scanButton.addEventListener('click', () => {
scannedCode = '';
scannedCodeDisplay.textContent = '';
setState('scanning');
scannerInput.value = '';
scannerInput.focus();
});
// ==========================================
// SCANNER INPUT HANDLING
// ==========================================
scannerInput.addEventListener('input', (e) => {
// Update display with current value
scannedCode = e.target.value;
scannedCodeDisplay.textContent = scannedCode;
// Reset timeout on each keystroke
if (scanTimeout) {
clearTimeout(scanTimeout);
}
// After 200ms of no input, consider scan complete
scanTimeout = setTimeout(() => {
if (scannedCode.length >= 8) {
processScannedCode(scannedCode);
}
}, 200);
});
// Handle Enter/Tab from scanner
scannerInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault();
if (scannedCode.length >= 8) {
clearTimeout(scanTimeout);
processScannedCode(scannedCode);
}
}
});
// Blur handling - if user clicks away during scanning
scannerInput.addEventListener('blur', () => {
if (currentState === 'scanning' && scannedCode.length === 0) {
// No code scanned, return to ready
setTimeout(() => {
if (currentState === 'scanning') {
setState('ready');
}
}, 500);
}
});
// ==========================================
// PROCESS SCANNED CODE
// ==========================================
async function processScannedCode(code) {
setState('loading');
document.getElementById('loadingCode').textContent = code;
// Simulate API delay
await new Promise(r => setTimeout(r, 1500));
// Look up product
foundProduct = mockProducts[code] || null;
if (foundProduct) {
// Show success state
document.getElementById('previewName').textContent = foundProduct.name;
document.getElementById('previewBrand').textContent = getBrandDisplayName(foundProduct.brand);
document.getElementById('previewCategory').textContent = getCategoryDisplayName(foundProduct.category);
document.getElementById('previewDescription').textContent = foundProduct.description;
setState('success');
} else {
// Show not found state
document.getElementById('notFoundCode').textContent = code;
// Still fill in the EAN field
document.getElementById('productEan').value = code;
setState('notfound');
}
}
function getBrandDisplayName(brand) {
const brands = {
'redken': 'Redken',
'olaplex': 'Olaplex',
'kerastase': 'Kérastase',
'wella': 'Wella',
'loreal': 'L\'Oréal'
};
return brands[brand] || brand;
}
function getCategoryDisplayName(category) {
const categories = {
'haarpleje': 'Hårpleje',
'styling': 'Styling',
'farve': 'Farve',
'tilbehoer': 'Tilbehør'
};
return categories[category] || category;
}
// ==========================================
// USE PRODUCT BUTTON
// ==========================================
document.getElementById('useProductBtn').addEventListener('click', () => {
if (foundProduct) {
// Fill in the form
document.getElementById('productName').value = foundProduct.name;
document.getElementById('productEan').value = scannedCode;
document.getElementById('productDescription').value = foundProduct.description;
// Set selects
const categorySelect = document.getElementById('productCategory');
const brandSelect = document.getElementById('productBrand');
for (let option of categorySelect.options) {
if (option.value === foundProduct.category) {
option.selected = true;
break;
}
}
for (let option of brandSelect.options) {
if (option.value === foundProduct.brand) {
option.selected = true;
break;
}
}
// Reset scanner to ready
setState('ready');
scannedCode = '';
foundProduct = null;
// Scroll to form and highlight
document.getElementById('productName').focus();
}
});
// ==========================================
// SCAN AGAIN BUTTONS
// ==========================================
document.getElementById('scanAgainBtn').addEventListener('click', () => {
scannedCode = '';
scannedCodeDisplay.textContent = '';
foundProduct = null;
setState('scanning');
scannerInput.value = '';
scannerInput.focus();
});
document.getElementById('scanAgainBtn2').addEventListener('click', () => {
scannedCode = '';
scannedCodeDisplay.textContent = '';
setState('scanning');
scannerInput.value = '';
scannerInput.focus();
});
// ==========================================
// FILL MANUAL BUTTON
// ==========================================
document.getElementById('fillManualBtn').addEventListener('click', () => {
setState('ready');
document.getElementById('productName').focus();
});
// ==========================================
// TOGGLE SLIDER
// ==========================================
document.querySelectorAll('swp-toggle-slider').forEach(slider => {
const options = slider.querySelectorAll('swp-toggle-option');
options.forEach((option, index) => {
option.addEventListener('click', () => {
slider.dataset.value = index === 0 ? 'yes' : 'no';
});
});
});
// ==========================================
// MARGIN CALCULATION
// ==========================================
const purchaseInput = document.getElementById('purchasePrice');
const saleInput = document.getElementById('salePrice');
const marginAmount = document.getElementById('marginAmount');
const marginPercent = document.getElementById('marginPercent');
function parsePrice(value) {
return parseFloat(value.replace(/[^\d,]/g, '').replace(',', '.')) || 0;
}
function formatPrice(value) {
return value.toFixed(2).replace('.', ',') + ' kr';
}
function calculateMargin() {
const purchase = parsePrice(purchaseInput.value);
const sale = parsePrice(saleInput.value);
if (purchase > 0 && sale > 0) {
const saleExVat = sale / 1.25; // Remove 25% VAT
const margin = saleExVat - purchase;
const percent = (margin / saleExVat) * 100;
marginAmount.textContent = formatPrice(margin);
marginPercent.textContent = percent.toFixed(1) + '%';
} else {
marginAmount.textContent = '0,00 kr';
marginPercent.textContent = '0%';
}
}
purchaseInput.addEventListener('input', calculateMargin);
saleInput.addEventListener('input', calculateMargin);
</script>
</body>
</html>