Enhances service management UI with dynamic pricing controls
Adds interactive price and duration adjustment for services Introduces visual indicators for overridden service values Improves editing experience with responsive and intuitive controls Implements granular service modifications with strikethrough and highlight effects
This commit is contained in:
parent
439903fda4
commit
85b006e0d6
1 changed files with 532 additions and 24 deletions
|
|
@ -961,6 +961,199 @@
|
||||||
color: var(--color-teal);
|
color: var(--color-teal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Service values (read mode) */
|
||||||
|
swp-service-values {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-price,
|
||||||
|
swp-service-duration {
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-price {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-duration {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-price.modified,
|
||||||
|
swp-service-duration.modified {
|
||||||
|
color: var(--color-teal);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide values in edit mode */
|
||||||
|
.edit-mode swp-service-values {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Service controls container - hidden by default */
|
||||||
|
swp-service-controls {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show controls in edit mode */
|
||||||
|
.edit-mode swp-service-controls {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Final values with adjustable controls */
|
||||||
|
swp-service-final-values {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-adjustable-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-adjustable-value.modified {
|
||||||
|
border-color: var(--color-teal);
|
||||||
|
background: color-mix(in srgb, var(--color-teal) 5%, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-adjustable-value .adjust-down,
|
||||||
|
swp-adjustable-value .adjust-up {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
background: var(--color-background);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 100ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-adjustable-value .adjust-down:hover,
|
||||||
|
swp-adjustable-value .adjust-up:hover {
|
||||||
|
background: var(--color-teal);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-adjustable-value .adjust-down:active,
|
||||||
|
swp-adjustable-value .adjust-up:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-adjustable-value .adjust-display {
|
||||||
|
min-width: 55px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text);
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-adjustable-value.modified .adjust-display {
|
||||||
|
color: var(--color-teal);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Duration display */
|
||||||
|
.duration-display {
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-display.modified {
|
||||||
|
color: var(--color-teal);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Original values (read mode) */
|
||||||
|
swp-service-originals {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-originals .original-price-val,
|
||||||
|
swp-service-originals .original-duration-val {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Strikethrough only when value is changed */
|
||||||
|
swp-service-originals .original-price-val.struck,
|
||||||
|
swp-service-originals .original-duration-val.struck {
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-color: var(--color-teal);
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Original values (edit mode) - hidden by default, shown when has-override */
|
||||||
|
swp-service-originals-edit {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-row.has-override swp-service-originals-edit {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-originals-edit .original-price-val,
|
||||||
|
swp-service-originals-edit .original-duration-val {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-service-originals-edit .original-price-val.struck,
|
||||||
|
swp-service-originals-edit .original-duration-val.struck {
|
||||||
|
text-decoration: line-through;
|
||||||
|
text-decoration-color: var(--color-teal);
|
||||||
|
text-decoration-thickness: 1px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Highlight originals when value changes */
|
||||||
|
swp-service-originals.highlight .original-price-val,
|
||||||
|
swp-service-originals.highlight .original-duration-val,
|
||||||
|
swp-service-originals-edit.highlight .original-price-val,
|
||||||
|
swp-service-originals-edit.highlight .original-duration-val {
|
||||||
|
animation: flash-text 300ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-border {
|
||||||
|
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-teal) 40%, transparent); }
|
||||||
|
100% { box-shadow: 0 0 0 4px transparent; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes flash-text {
|
||||||
|
0% { opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
LAYOUT GRID
|
LAYOUT GRID
|
||||||
========================================== */
|
========================================== */
|
||||||
|
|
@ -1033,6 +1226,13 @@
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swp-stat-subtitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
swp-stat-card.highlight swp-stat-value {
|
swp-stat-card.highlight swp-stat-value {
|
||||||
color: var(--color-teal);
|
color: var(--color-teal);
|
||||||
}
|
}
|
||||||
|
|
@ -1589,6 +1789,90 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
INVOICE TABLE (afsluttede bookinger)
|
||||||
|
========================================== */
|
||||||
|
swp-invoice-table {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table th,
|
||||||
|
swp-invoice-table td {
|
||||||
|
padding: 12px 16px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table th {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table td {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table .date,
|
||||||
|
swp-invoice-table .time {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table .customer {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table .services {
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table .duration {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table .amount {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-invoice-table .amount-col {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status badges */
|
||||||
|
swp-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-status-badge.paid {
|
||||||
|
background: color-mix(in srgb, var(--color-teal) 15%, white);
|
||||||
|
color: var(--color-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-status-badge.pending {
|
||||||
|
background: color-mix(in srgb, #f59e0b 15%, white);
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
/* ==========================================
|
/* ==========================================
|
||||||
CHART SECTION (matches poc-detail-drawer.html)
|
CHART SECTION (matches poc-detail-drawer.html)
|
||||||
========================================== */
|
========================================== */
|
||||||
|
|
@ -2360,12 +2644,13 @@
|
||||||
<swp-stat-label>Bookinger denne måned</swp-stat-label>
|
<swp-stat-label>Bookinger denne måned</swp-stat-label>
|
||||||
</swp-stat-card>
|
</swp-stat-card>
|
||||||
<swp-stat-card>
|
<swp-stat-card>
|
||||||
<swp-stat-value>28.450 kr</swp-stat-value>
|
<swp-stat-value>30.825 kr</swp-stat-value>
|
||||||
<swp-stat-label>Omsætning denne måned</swp-stat-label>
|
<swp-stat-label>Værdi af bookede services</swp-stat-label>
|
||||||
|
<swp-stat-subtitle>Baseret på 49 bookinger</swp-stat-subtitle>
|
||||||
</swp-stat-card>
|
</swp-stat-card>
|
||||||
<swp-stat-card>
|
<swp-stat-card>
|
||||||
<swp-stat-value>4.9</swp-stat-value>
|
<swp-stat-value>28.450 kr</swp-stat-value>
|
||||||
<swp-stat-label>Gns. kundetilfredshed</swp-stat-label>
|
<swp-stat-label>Omsætning denne måned</swp-stat-label>
|
||||||
</swp-stat-card>
|
</swp-stat-card>
|
||||||
<swp-stat-card>
|
<swp-stat-card>
|
||||||
<swp-stat-value>68%</swp-stat-value>
|
<swp-stat-value>68%</swp-stat-value>
|
||||||
|
|
@ -2441,6 +2726,100 @@
|
||||||
</swp-booking-table>
|
</swp-booking-table>
|
||||||
</swp-card>
|
</swp-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Afsluttede bookinger (fakturaliste) -->
|
||||||
|
<swp-card style="margin-top: 24px;">
|
||||||
|
<swp-section-label>Afsluttede bookinger</swp-section-label>
|
||||||
|
<swp-invoice-table>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Dato</th>
|
||||||
|
<th>Tid</th>
|
||||||
|
<th>Kunde</th>
|
||||||
|
<th>Services</th>
|
||||||
|
<th>Varighed</th>
|
||||||
|
<th class="amount-col">Beløb</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="date">23. dec 2024</td>
|
||||||
|
<td class="time">10:00</td>
|
||||||
|
<td class="customer">Maria Hansen</td>
|
||||||
|
<td class="services">Dameklip, Bundfarve</td>
|
||||||
|
<td class="duration">2t 30m</td>
|
||||||
|
<td class="amount">1.510 kr</td>
|
||||||
|
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="date">23. dec 2024</td>
|
||||||
|
<td class="time">13:30</td>
|
||||||
|
<td class="customer">Louise Nielsen</td>
|
||||||
|
<td class="services">Balayage langt hår, Olaplex</td>
|
||||||
|
<td class="duration">3t</td>
|
||||||
|
<td class="amount">2.700 kr</td>
|
||||||
|
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="date">22. dec 2024</td>
|
||||||
|
<td class="time">09:00</td>
|
||||||
|
<td class="customer">Sofie Andersen</td>
|
||||||
|
<td class="services">Dameklip</td>
|
||||||
|
<td class="duration">1t</td>
|
||||||
|
<td class="amount">725 kr</td>
|
||||||
|
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="date">22. dec 2024</td>
|
||||||
|
<td class="time">11:00</td>
|
||||||
|
<td class="customer">Karen Pedersen</td>
|
||||||
|
<td class="services">Striber mellemlangt hår, Klip</td>
|
||||||
|
<td class="duration">2t 30m</td>
|
||||||
|
<td class="amount">2.390 kr</td>
|
||||||
|
<td><swp-status-badge class="pending">Afventer</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="date">21. dec 2024</td>
|
||||||
|
<td class="time">14:00</td>
|
||||||
|
<td class="customer">Emma Larsen</td>
|
||||||
|
<td class="services">Olaplex Stand alone</td>
|
||||||
|
<td class="duration">1t</td>
|
||||||
|
<td class="amount">550 kr</td>
|
||||||
|
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="date">21. dec 2024</td>
|
||||||
|
<td class="time">10:00</td>
|
||||||
|
<td class="customer">Mette Kristensen</td>
|
||||||
|
<td class="services">Herreklip</td>
|
||||||
|
<td class="duration">1t</td>
|
||||||
|
<td class="amount">645 kr</td>
|
||||||
|
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="date">20. dec 2024</td>
|
||||||
|
<td class="time">09:30</td>
|
||||||
|
<td class="customer">Anne Thomsen</td>
|
||||||
|
<td class="services">Glossing mellemlangt hår</td>
|
||||||
|
<td class="duration">1t</td>
|
||||||
|
<td class="amount">745 kr</td>
|
||||||
|
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="date">20. dec 2024</td>
|
||||||
|
<td class="time">12:00</td>
|
||||||
|
<td class="customer">Lise Mortensen</td>
|
||||||
|
<td class="services">Dameklip, Farvning vipper & bryn</td>
|
||||||
|
<td class="duration">1t 30m</td>
|
||||||
|
<td class="amount">1.070 kr</td>
|
||||||
|
<td><swp-status-badge class="paid">Betalt</swp-status-badge></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</swp-invoice-table>
|
||||||
|
</swp-card>
|
||||||
</swp-tab-content>
|
</swp-tab-content>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
|
@ -2569,33 +2948,72 @@
|
||||||
return `${minutes}m`;
|
return `${minutes}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format duration with offset (e.g., "1t -5m" or "30m +10m")
|
||||||
|
function formatDurationWithOffset(baseMinutes, offset) {
|
||||||
|
const baseStr = formatDuration(baseMinutes);
|
||||||
|
if (offset === 0) return baseStr;
|
||||||
|
const sign = offset > 0 ? '+' : '';
|
||||||
|
return `${baseStr} <span class="duration-offset">${sign}${offset}m</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Create service row HTML
|
// Create service row HTML
|
||||||
function createServiceRow(service, isSelected) {
|
function createServiceRow(service, isSelected) {
|
||||||
const state = serviceState.get(service.id);
|
const state = serviceState.get(service.id);
|
||||||
const effectivePrice = state.priceOverride !== null ? state.priceOverride : service.price;
|
const effectivePrice = state.priceOverride !== null ? state.priceOverride : service.price;
|
||||||
const effectiveDuration = service.duration + state.durationOverride;
|
const effectiveDuration = service.duration + state.durationOverride;
|
||||||
|
const hasPriceOverride = state.priceOverride !== null && state.priceOverride !== service.price;
|
||||||
|
const hasDurationOverride = state.durationOverride !== 0;
|
||||||
|
|
||||||
const row = document.createElement('swp-service-row');
|
const row = document.createElement('swp-service-row');
|
||||||
row.dataset.id = service.id;
|
row.dataset.id = service.id;
|
||||||
row.dataset.category = service.category;
|
row.dataset.category = service.category;
|
||||||
row.draggable = true;
|
row.draggable = true;
|
||||||
|
|
||||||
if (state.priceOverride !== null || state.durationOverride !== 0) {
|
if (hasPriceOverride || hasDurationOverride) {
|
||||||
row.classList.add('has-override');
|
row.classList.add('has-override');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
// Selected: with editable inputs
|
// Selected: show adjusted values + original values in bordered box when modified
|
||||||
|
const hasAnyOverride = hasPriceOverride || hasDurationOverride;
|
||||||
|
|
||||||
|
// Original values box (shown when any override exists)
|
||||||
|
const originalsHtml = hasAnyOverride
|
||||||
|
? `<swp-service-originals>
|
||||||
|
<span class="original-price-val ${hasPriceOverride ? 'struck' : ''}" data-original-price="${service.price}">${service.price} kr</span>
|
||||||
|
<span class="original-duration-val ${hasDurationOverride ? 'struck' : ''}" data-original-duration="${service.duration}">${formatDuration(service.duration)}</span>
|
||||||
|
</swp-service-originals>`
|
||||||
|
: '';
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<swp-drag-handle>⋮⋮</swp-drag-handle>
|
<swp-drag-handle>⋮⋮</swp-drag-handle>
|
||||||
<swp-service-color style="background: ${service.color};"></swp-service-color>
|
<swp-service-color style="background: ${service.color};"></swp-service-color>
|
||||||
<swp-service-info>
|
<swp-service-info>
|
||||||
<swp-service-name>${service.name}</swp-service-name>
|
<swp-service-name>${service.name}</swp-service-name>
|
||||||
</swp-service-info>
|
</swp-service-info>
|
||||||
<swp-service-price-col>
|
<swp-service-values>
|
||||||
<input type="number" value="${effectivePrice}" data-original="${service.price}"> kr
|
${originalsHtml}
|
||||||
</swp-service-price-col>
|
<swp-service-price class="${hasPriceOverride ? 'modified' : ''}">${effectivePrice} kr</swp-service-price>
|
||||||
<swp-service-duration-col>${formatDuration(effectiveDuration)}</swp-service-duration-col>
|
<swp-service-duration class="${hasDurationOverride ? 'modified' : ''}">${formatDuration(effectiveDuration)}</swp-service-duration>
|
||||||
|
</swp-service-values>
|
||||||
|
<swp-service-controls>
|
||||||
|
<swp-service-originals-edit>
|
||||||
|
<span class="original-price-val ${hasPriceOverride ? 'struck' : ''}">${service.price} kr</span>
|
||||||
|
<span class="original-duration-val ${hasDurationOverride ? 'struck' : ''}">${formatDuration(service.duration)}</span>
|
||||||
|
</swp-service-originals-edit>
|
||||||
|
<swp-service-final-values>
|
||||||
|
<swp-adjustable-value class="price-adjust ${hasPriceOverride ? 'modified' : ''}" data-type="price" data-original="${service.price}" data-value="${effectivePrice}" data-step="5">
|
||||||
|
<button class="adjust-down" type="button">−</button>
|
||||||
|
<span class="adjust-display">${effectivePrice} kr</span>
|
||||||
|
<button class="adjust-up" type="button">+</button>
|
||||||
|
</swp-adjustable-value>
|
||||||
|
<swp-adjustable-value class="duration-adjust ${hasDurationOverride ? 'modified' : ''}" data-type="duration" data-base="${service.duration}" data-offset="${state.durationOverride}" data-step="5">
|
||||||
|
<button class="adjust-down" type="button">−</button>
|
||||||
|
<span class="adjust-display">${formatDuration(effectiveDuration)}</span>
|
||||||
|
<button class="adjust-up" type="button">+</button>
|
||||||
|
</swp-adjustable-value>
|
||||||
|
</swp-service-final-values>
|
||||||
|
</swp-service-controls>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Available: name only (no price/duration)
|
// Available: name only (no price/duration)
|
||||||
|
|
@ -2840,22 +3258,110 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Price input handler
|
// Adjustable value click handler (up/down buttons)
|
||||||
selectedContainer.addEventListener('input', (e) => {
|
selectedContainer.addEventListener('click', (e) => {
|
||||||
if (e.target.type === 'number') {
|
const btn = e.target.closest('.adjust-up, .adjust-down');
|
||||||
const row = e.target.closest('swp-service-row');
|
if (!btn) return;
|
||||||
const serviceId = parseInt(row.dataset.id);
|
|
||||||
const state = serviceState.get(serviceId);
|
|
||||||
const originalPrice = parseInt(e.target.dataset.original);
|
|
||||||
const newPrice = parseInt(e.target.value) || originalPrice;
|
|
||||||
|
|
||||||
if (newPrice !== originalPrice) {
|
const adjustable = btn.closest('swp-adjustable-value');
|
||||||
state.priceOverride = newPrice;
|
const row = btn.closest('swp-service-row');
|
||||||
row.classList.add('has-override');
|
if (!adjustable || !row) return;
|
||||||
} else {
|
|
||||||
state.priceOverride = null;
|
const serviceId = parseInt(row.dataset.id);
|
||||||
row.classList.remove('has-override');
|
const state = serviceState.get(serviceId);
|
||||||
|
const service = allServices.find(s => s.id === serviceId);
|
||||||
|
const step = parseInt(adjustable.dataset.step);
|
||||||
|
const isUp = btn.classList.contains('adjust-up');
|
||||||
|
const delta = isUp ? step : -step;
|
||||||
|
|
||||||
|
if (adjustable.dataset.type === 'price') {
|
||||||
|
// Adjust price
|
||||||
|
const original = parseInt(adjustable.dataset.original);
|
||||||
|
let current = parseInt(adjustable.dataset.value);
|
||||||
|
current = Math.max(0, current + delta);
|
||||||
|
adjustable.dataset.value = current;
|
||||||
|
|
||||||
|
const isModified = current !== original;
|
||||||
|
state.priceOverride = isModified ? current : null;
|
||||||
|
|
||||||
|
// Update displays
|
||||||
|
adjustable.querySelector('.adjust-display').textContent = `${current} kr`;
|
||||||
|
adjustable.classList.toggle('modified', isModified);
|
||||||
|
|
||||||
|
const priceDisplay = row.querySelector('swp-service-price');
|
||||||
|
if (priceDisplay) {
|
||||||
|
priceDisplay.textContent = `${current} kr`;
|
||||||
|
priceDisplay.classList.toggle('modified', isModified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle strikethrough on all original price elements (read + edit mode)
|
||||||
|
row.querySelectorAll('.original-price-val').forEach(el => {
|
||||||
|
el.classList.toggle('struck', isModified);
|
||||||
|
});
|
||||||
|
} else if (adjustable.dataset.type === 'duration') {
|
||||||
|
// Adjust duration offset
|
||||||
|
const base = parseInt(adjustable.dataset.base);
|
||||||
|
let offset = parseInt(adjustable.dataset.offset);
|
||||||
|
offset = Math.max(-base, offset + delta); // Don't go below 0 total
|
||||||
|
adjustable.dataset.offset = offset;
|
||||||
|
|
||||||
|
const effectiveDuration = base + offset;
|
||||||
|
const isModified = offset !== 0;
|
||||||
|
state.durationOverride = offset;
|
||||||
|
|
||||||
|
// Update displays
|
||||||
|
adjustable.querySelector('.adjust-display').textContent = formatDuration(effectiveDuration);
|
||||||
|
adjustable.classList.toggle('modified', isModified);
|
||||||
|
|
||||||
|
const durationDisplay = row.querySelector('swp-service-duration');
|
||||||
|
if (durationDisplay) {
|
||||||
|
durationDisplay.textContent = formatDuration(effectiveDuration);
|
||||||
|
durationDisplay.classList.toggle('modified', isModified);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle strikethrough on all original duration elements (read + edit mode)
|
||||||
|
row.querySelectorAll('.original-duration-val').forEach(el => {
|
||||||
|
el.classList.toggle('struck', isModified);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any override exists
|
||||||
|
const hasOverride = state.priceOverride !== null || state.durationOverride !== 0;
|
||||||
|
row.classList.toggle('has-override', hasOverride);
|
||||||
|
|
||||||
|
// Handle originals box visibility and highlight
|
||||||
|
const valuesContainer = row.querySelector('swp-service-values');
|
||||||
|
let originalsBox = valuesContainer.querySelector('swp-service-originals');
|
||||||
|
const originalsEditBox = row.querySelector('swp-service-originals-edit');
|
||||||
|
|
||||||
|
if (hasOverride) {
|
||||||
|
// Show originals box if not present (insert at beginning)
|
||||||
|
const hasPriceOverride = state.priceOverride !== null;
|
||||||
|
const hasDurationOverride = state.durationOverride !== 0;
|
||||||
|
|
||||||
|
if (!originalsBox) {
|
||||||
|
originalsBox = document.createElement('swp-service-originals');
|
||||||
|
originalsBox.innerHTML = `
|
||||||
|
<span class="original-price-val ${hasPriceOverride ? 'struck' : ''}">${service.price} kr</span>
|
||||||
|
<span class="original-duration-val ${hasDurationOverride ? 'struck' : ''}">${formatDuration(service.duration)}</span>
|
||||||
|
`;
|
||||||
|
valuesContainer.insertBefore(originalsBox, valuesContainer.firstChild);
|
||||||
|
} else {
|
||||||
|
// Update struck classes on existing originals box
|
||||||
|
const priceVal = originalsBox.querySelector('.original-price-val');
|
||||||
|
const durationVal = originalsBox.querySelector('.original-duration-val');
|
||||||
|
if (priceVal) priceVal.classList.toggle('struck', hasPriceOverride);
|
||||||
|
if (durationVal) durationVal.classList.toggle('struck', hasDurationOverride);
|
||||||
|
}
|
||||||
|
// Highlight animation
|
||||||
|
originalsBox.classList.remove('highlight');
|
||||||
|
originalsEditBox?.classList.remove('highlight');
|
||||||
|
void originalsBox.offsetWidth; // Trigger reflow
|
||||||
|
originalsBox.classList.add('highlight');
|
||||||
|
originalsEditBox?.classList.add('highlight');
|
||||||
|
} else {
|
||||||
|
// Remove originals box if no overrides
|
||||||
|
if (originalsBox) originalsBox.remove();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2875,11 +3381,13 @@
|
||||||
const servicesPanel = document.getElementById('servicesPanel');
|
const servicesPanel = document.getElementById('servicesPanel');
|
||||||
const panelToggle = document.getElementById('panelToggle');
|
const panelToggle = document.getElementById('panelToggle');
|
||||||
const editServicesBtn = document.getElementById('editServicesBtn');
|
const editServicesBtn = document.getElementById('editServicesBtn');
|
||||||
|
const selectedServicesCard = document.querySelector('.selected-services-card');
|
||||||
|
|
||||||
function toggleServicesPanel() {
|
function toggleServicesPanel() {
|
||||||
const isCollapsed = servicesPanel.classList.contains('collapsed');
|
const isCollapsed = servicesPanel.classList.contains('collapsed');
|
||||||
servicesPanel.classList.toggle('collapsed', !isCollapsed);
|
servicesPanel.classList.toggle('collapsed', !isCollapsed);
|
||||||
servicesPanel.classList.toggle('expanded', isCollapsed);
|
servicesPanel.classList.toggle('expanded', isCollapsed);
|
||||||
|
selectedServicesCard.classList.toggle('edit-mode', isCollapsed);
|
||||||
editServicesBtn.classList.toggle('active', isCollapsed);
|
editServicesBtn.classList.toggle('active', isCollapsed);
|
||||||
editServicesBtn.innerHTML = isCollapsed
|
editServicesBtn.innerHTML = isCollapsed
|
||||||
? `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> Luk`
|
? `<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg> Luk`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue