Adds Online Booking configuration and preview components

Introduces comprehensive online booking feature with:
- Localization support for booking settings
- ViewComponents for booking URL, settings, company info, hours, and preview
- Responsive preview with device toggle functionality
- Integrated with settings page and translation files

Enhances application's online booking configuration interface
This commit is contained in:
Janus C. H. Knudsen 2026-01-28 20:17:54 +01:00
parent eba6bd646d
commit 435d9f11b7
17 changed files with 891 additions and 25 deletions

View file

@ -0,0 +1,33 @@
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-buildings"></i>
<span localize="onlineBooking.company.title">Virksomhedsoplysninger</span>
</swp-card-title>
<swp-section-action href="/indstillinger" localize="onlineBooking.company.edit">Rediger</swp-section-action>
</swp-card-header>
<swp-card-content>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label localize="onlineBooking.company.name">Navn</swp-edit-label>
<swp-edit-value>Salon Beauty</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label localize="onlineBooking.company.address">Adresse</swp-edit-label>
<swp-edit-value>Hovedgaden 123</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label localize="onlineBooking.company.zipCity">Postnr + By</swp-edit-label>
<swp-edit-value>2100 København Ø</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label localize="onlineBooking.company.phone">Telefon</swp-edit-label>
<swp-edit-value>+45 12 34 56 78</swp-edit-value>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label localize="onlineBooking.company.email">Email</swp-edit-label>
<swp-edit-value>kontakt@salonbeauty.dk</swp-edit-value>
</swp-edit-row>
</swp-edit-section>
</swp-card-content>
</swp-card>

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.OnlineBooking.Components;
/// <summary>
/// ViewComponent for displaying company information used in booking.
/// </summary>
public class OnlineBookingCompanyViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View file

@ -0,0 +1,93 @@
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-clock"></i>
<span localize="onlineBooking.hours.title">Åbningstider</span>
</swp-card-title>
<swp-section-action href="/indstillinger" localize="onlineBooking.hours.edit">Rediger</swp-section-action>
</swp-card-header>
<swp-card-content>
<swp-hours-table>
<swp-hours-row>
<swp-hours-day localize="employees.detail.hours.monday">Mandag</swp-hours-day>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
<swp-hours-time>
<input type="time" value="09:00">
<span>-</span>
<input type="time" value="17:00">
</swp-hours-time>
</swp-hours-row>
<swp-hours-row>
<swp-hours-day localize="employees.detail.hours.tuesday">Tirsdag</swp-hours-day>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
<swp-hours-time>
<input type="time" value="09:00">
<span>-</span>
<input type="time" value="17:00">
</swp-hours-time>
</swp-hours-row>
<swp-hours-row>
<swp-hours-day localize="employees.detail.hours.wednesday">Onsdag</swp-hours-day>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
<swp-hours-time>
<input type="time" value="09:00">
<span>-</span>
<input type="time" value="17:00">
</swp-hours-time>
</swp-hours-row>
<swp-hours-row>
<swp-hours-day localize="employees.detail.hours.thursday">Torsdag</swp-hours-day>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
<swp-hours-time>
<input type="time" value="09:00">
<span>-</span>
<input type="time" value="19:00">
</swp-hours-time>
</swp-hours-row>
<swp-hours-row>
<swp-hours-day localize="employees.detail.hours.friday">Fredag</swp-hours-day>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
<swp-hours-time>
<input type="time" value="09:00">
<span>-</span>
<input type="time" value="17:00">
</swp-hours-time>
</swp-hours-row>
<swp-hours-row>
<swp-hours-day localize="employees.detail.hours.saturday">Lørdag</swp-hours-day>
<swp-toggle-slider data-value="yes">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
<swp-hours-time>
<input type="time" value="10:00">
<span>-</span>
<input type="time" value="14:00">
</swp-hours-time>
</swp-hours-row>
<swp-hours-row>
<swp-hours-day localize="employees.detail.hours.sunday">Søndag</swp-hours-day>
<swp-toggle-slider data-value="no">
<swp-toggle-option>Ja</swp-toggle-option>
<swp-toggle-option>Nej</swp-toggle-option>
</swp-toggle-slider>
<swp-hours-closed localize="onlineBooking.hours.closed">Lukket</swp-hours-closed>
</swp-hours-row>
</swp-hours-table>
</swp-card-content>
</swp-card>

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.OnlineBooking.Components;
/// <summary>
/// ViewComponent for displaying opening hours used in booking.
/// </summary>
public class OnlineBookingHoursViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View file

@ -0,0 +1,130 @@
<swp-online-booking-preview-container>
<swp-preview-header>
<swp-preview-title>
<i class="ph ph-eye"></i>
<span localize="onlineBooking.preview.title">Preview</span>
</swp-preview-title>
<swp-device-toggle>
<swp-device-btn class="active" data-device="desktop" title="Desktop">
<i class="ph ph-desktop"></i>
</swp-device-btn>
<swp-device-btn data-device="tablet" title="Tablet">
<i class="ph ph-device-tablet"></i>
</swp-device-btn>
<swp-device-btn data-device="mobile" title="Mobil">
<i class="ph ph-device-mobile"></i>
</swp-device-btn>
</swp-device-toggle>
</swp-preview-header>
<swp-preview-frame data-device="desktop">
<swp-preview-content>
<!-- Placeholder booking UI -->
<swp-booking-demo>
<swp-booking-demo-header>
<swp-booking-demo-logo>SB</swp-booking-demo-logo>
<swp-booking-demo-info>
<h2>Salon Beauty</h2>
<p>Hovedgaden 123, 2100 København Ø</p>
</swp-booking-demo-info>
</swp-booking-demo-header>
<swp-booking-demo-section>
<h3>Vælg service</h3>
<swp-booking-demo-services>
<swp-booking-demo-service class="selected">
<span class="name">Dame klip</span>
<span class="meta">45 min · 450 kr</span>
</swp-booking-demo-service>
<swp-booking-demo-service>
<span class="name">Herre klip</span>
<span class="meta">30 min · 350 kr</span>
</swp-booking-demo-service>
<swp-booking-demo-service>
<span class="name">Farvning</span>
<span class="meta">90 min · 850 kr</span>
</swp-booking-demo-service>
<swp-booking-demo-service>
<span class="name">Balayage</span>
<span class="meta">120 min · 1.450 kr</span>
</swp-booking-demo-service>
</swp-booking-demo-services>
</swp-booking-demo-section>
<swp-booking-demo-section>
<h3>Vælg medarbejder</h3>
<swp-booking-demo-employees>
<swp-booking-demo-employee class="selected">
<swp-avatar class="size-sm">MJ</swp-avatar>
<span>Maria Jensen</span>
</swp-booking-demo-employee>
<swp-booking-demo-employee>
<swp-avatar class="size-sm purple">AH</swp-avatar>
<span>Anna Hansen</span>
</swp-booking-demo-employee>
<swp-booking-demo-employee>
<swp-avatar class="size-sm blue">LP</swp-avatar>
<span>Louise Petersen</span>
</swp-booking-demo-employee>
</swp-booking-demo-employees>
</swp-booking-demo-section>
<swp-booking-demo-section>
<h3>Vælg dato og tid</h3>
<swp-booking-demo-calendar>
<swp-booking-demo-month>Januar 2026</swp-booking-demo-month>
<swp-booking-demo-days>
<span class="day-header">Ma</span>
<span class="day-header">Ti</span>
<span class="day-header">On</span>
<span class="day-header">To</span>
<span class="day-header">Fr</span>
<span class="day-header">Lø</span>
<span class="day-header">Sø</span>
<span class="day other">27</span>
<span class="day other">28</span>
<span class="day other">29</span>
<span class="day other">30</span>
<span class="day other">31</span>
<span class="day">1</span>
<span class="day">2</span>
<span class="day">3</span>
<span class="day">4</span>
<span class="day">5</span>
<span class="day">6</span>
<span class="day">7</span>
<span class="day">8</span>
<span class="day">9</span>
<span class="day">10</span>
<span class="day">11</span>
<span class="day">12</span>
<span class="day">13</span>
<span class="day selected">14</span>
<span class="day">15</span>
<span class="day">16</span>
</swp-booking-demo-days>
</swp-booking-demo-calendar>
</swp-booking-demo-section>
<swp-booking-demo-footer>
<swp-btn class="primary full-width">Book tid</swp-btn>
</swp-booking-demo-footer>
</swp-booking-demo>
</swp-preview-content>
</swp-preview-frame>
</swp-online-booking-preview-container>
<script>
(function() {
const deviceBtns = document.querySelectorAll('swp-device-btn');
const previewFrame = document.querySelector('swp-preview-frame');
deviceBtns.forEach(btn => {
btn.addEventListener('click', () => {
deviceBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
previewFrame.setAttribute('data-device', btn.getAttribute('data-device'));
});
});
})();
</script>

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.OnlineBooking.Components;
/// <summary>
/// ViewComponent for the booking preview iframe with device toggle.
/// </summary>
public class OnlineBookingPreviewViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View file

@ -0,0 +1,61 @@
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-sliders"></i>
<span localize="onlineBooking.settings.title">Booking-indstillinger</span>
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-toggle-row>
<swp-toggle-info>
<swp-toggle-label localize="onlineBooking.settings.enableBooking">Aktivér online booking</swp-toggle-label>
<swp-toggle-desc localize="onlineBooking.settings.enableBookingDesc">Tillad kunder at booke tider online</swp-toggle-desc>
</swp-toggle-info>
<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-section-divider></swp-section-divider>
<swp-edit-section>
<swp-edit-row class="wide-label">
<swp-edit-label localize="onlineBooking.settings.bookAhead">Book frem i tiden</swp-edit-label>
<swp-edit-select>
<select id="booking-ahead">
<option value="7">7 dage</option>
<option value="14">14 dage</option>
<option value="30" selected>30 dage</option>
<option value="60">60 dage</option>
<option value="90">90 dage</option>
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row class="wide-label">
<swp-edit-label localize="onlineBooking.settings.minNotice">Minimum varsel</swp-edit-label>
<swp-edit-select>
<select id="min-notice">
<option value="0">Ingen begrænsning</option>
<option value="1">1 time</option>
<option value="2" selected>2 timer</option>
<option value="4">4 timer</option>
<option value="24">24 timer</option>
</select>
</swp-edit-select>
</swp-edit-row>
<swp-edit-row class="wide-label">
<swp-edit-label localize="onlineBooking.settings.cancelDeadline">Aflysningsfrist</swp-edit-label>
<swp-edit-select>
<select id="cancel-deadline">
<option value="0">Ingen frist</option>
<option value="2">2 timer før</option>
<option value="4">4 timer før</option>
<option value="24" selected>24 timer før</option>
<option value="48">48 timer før</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
</swp-card-content>
</swp-card>

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.OnlineBooking.Components;
/// <summary>
/// ViewComponent for online booking settings (toggle, booking ahead, notice, cancellation).
/// </summary>
public class OnlineBookingSettingsViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View file

@ -0,0 +1,31 @@
<swp-card>
<swp-card-header>
<swp-card-title>
<i class="ph ph-link"></i>
<span localize="onlineBooking.url.title">Booking URL</span>
</swp-card-title>
</swp-card-header>
<swp-card-content>
<swp-url-field>
<input type="text" value="https://book.plantempus.dk/salonbeauty" readonly id="booking-url">
<swp-url-copy title="Kopier link" onclick="copyBookingUrl()">
<i class="ph ph-copy"></i>
</swp-url-copy>
</swp-url-field>
<swp-url-actions>
<swp-btn class="secondary sm" onclick="window.open(document.getElementById('booking-url').value, '_blank')">
<i class="ph ph-arrow-square-out"></i>
<span localize="onlineBooking.url.open">Åbn i ny fane</span>
</swp-btn>
</swp-url-actions>
</swp-card-content>
</swp-card>
<script>
function copyBookingUrl() {
const input = document.getElementById('booking-url');
navigator.clipboard.writeText(input.value).then(() => {
// Could show a toast notification here
});
}
</script>

View file

@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Mvc;
namespace PlanTempus.Application.Features.OnlineBooking.Components;
/// <summary>
/// ViewComponent for displaying the booking URL with copy and open actions.
/// </summary>
public class OnlineBookingUrlViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}

View file

@ -0,0 +1,42 @@
@page "/online-booking"
@model PlanTempus.Application.Features.OnlineBooking.Pages.IndexModel
@{
ViewData["Title"] = "Online Booking";
}
<swp-sticky-header>
<swp-header-content>
<swp-page-header>
<swp-page-title>
<h1 localize="onlineBooking.title">Online Booking</h1>
<p localize="onlineBooking.subtitle">Konfigurer og preview din booking-side</p>
</swp-page-title>
<swp-page-actions>
<swp-status-indicator data-active="true">
<i class="ph ph-check-circle icon"></i>
<span localize="onlineBooking.status.active">Aktiv</span>
</swp-status-indicator>
</swp-page-actions>
</swp-page-header>
</swp-header-content>
</swp-sticky-header>
<swp-page-container>
<swp-online-booking-layout>
<!-- Column 1: Settings -->
<swp-online-booking-settings>
@await Component.InvokeAsync("OnlineBookingUrl")
@await Component.InvokeAsync("OnlineBookingSettings")
@await Component.InvokeAsync("OnlineBookingCompany")
@await Component.InvokeAsync("OnlineBookingHours")
<swp-all-settings-link href="/indstillinger">
<span localize="onlineBooking.allSettings">Alle indstillinger</span>
<i class="ph ph-arrow-right"></i>
</swp-all-settings-link>
</swp-online-booking-settings>
<!-- Column 2: Preview -->
@await Component.InvokeAsync("OnlineBookingPreview")
</swp-online-booking-layout>
</swp-page-container>

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace PlanTempus.Application.Features.OnlineBooking.Pages;
public class IndexModel : PageModel
{
public void OnGet()
{
}
}