Adds customer details drawer to customers list page
Enhances customer management with interactive drawer Introduces detailed customer profile view with metadata Implements search functionality and dynamic drawer population Improves user experience for customer information exploration
This commit is contained in:
parent
65ad9aacdf
commit
0a431c8db4
8 changed files with 694 additions and 15 deletions
|
|
@ -61,7 +61,7 @@
|
||||||
<swp-data-table-cell localize="customers.column.tags">Tags</swp-data-table-cell>
|
<swp-data-table-cell localize="customers.column.tags">Tags</swp-data-table-cell>
|
||||||
</swp-data-table-header>
|
</swp-data-table-header>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Anna Jensen">
|
<swp-data-table-row data-name="Anna Jensen" data-visits="6" data-created="2024-09" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>AJ</swp-avatar>
|
<swp-avatar>AJ</swp-avatar>
|
||||||
<span>Anna Jensen</span>
|
<span>Anna Jensen</span>
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
<swp-data-table-cell></swp-data-table-cell>
|
<swp-data-table-cell></swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Camilla Holm">
|
<swp-data-table-row data-name="Camilla Holm" data-visits="25" data-created="2022-12" data-tags="vip" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>CH</swp-avatar>
|
<swp-avatar>CH</swp-avatar>
|
||||||
<span>Camilla Holm</span>
|
<span>Camilla Holm</span>
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
</swp-data-table-cell>
|
</swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Emma Larsen">
|
<swp-data-table-row data-name="Emma Larsen" data-visits="8" data-created="2024-06" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>EL</swp-avatar>
|
<swp-avatar>EL</swp-avatar>
|
||||||
<span>Emma Larsen</span>
|
<span>Emma Larsen</span>
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
<swp-data-table-cell></swp-data-table-cell>
|
<swp-data-table-cell></swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Freja Christensen">
|
<swp-data-table-row data-name="Freja Christensen" data-visits="31" data-created="2022-08" data-tags="vip,allergi" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>FC</swp-avatar>
|
<swp-avatar>FC</swp-avatar>
|
||||||
<span>Freja Christensen</span>
|
<span>Freja Christensen</span>
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
</swp-data-table-cell>
|
</swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Ida Andersen">
|
<swp-data-table-row data-name="Ida Andersen" data-visits="3" data-created="2025-10" data-tags="ny" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>IA</swp-avatar>
|
<swp-avatar>IA</swp-avatar>
|
||||||
<span>Ida Andersen</span>
|
<span>Ida Andersen</span>
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
</swp-data-table-cell>
|
</swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Katrine Berg">
|
<swp-data-table-row data-name="Katrine Berg" data-visits="12" data-created="2024-04" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>KB</swp-avatar>
|
<swp-avatar>KB</swp-avatar>
|
||||||
<span>Katrine Berg</span>
|
<span>Katrine Berg</span>
|
||||||
|
|
@ -152,7 +152,7 @@
|
||||||
<swp-data-table-cell></swp-data-table-cell>
|
<swp-data-table-cell></swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Line Frost">
|
<swp-data-table-row data-name="Line Frost" data-visits="9" data-created="2024-05" data-tags="sensitiv" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>LF</swp-avatar>
|
<swp-avatar>LF</swp-avatar>
|
||||||
<span>Line Frost</span>
|
<span>Line Frost</span>
|
||||||
|
|
@ -168,7 +168,7 @@
|
||||||
</swp-data-table-cell>
|
</swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Louise Hansen">
|
<swp-data-table-row data-name="Louise Hansen" data-visits="18" data-created="2023-02" data-tags="stamkunde" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar class="purple">LH</swp-avatar>
|
<swp-avatar class="purple">LH</swp-avatar>
|
||||||
<span>Louise Hansen</span>
|
<span>Louise Hansen</span>
|
||||||
|
|
@ -184,7 +184,7 @@
|
||||||
</swp-data-table-cell>
|
</swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Maja Petersen">
|
<swp-data-table-row data-name="Maja Petersen" data-visits="22" data-created="2023-01" data-tags="stamkunde" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar class="blue">MP</swp-avatar>
|
<swp-avatar class="blue">MP</swp-avatar>
|
||||||
<span>Maja Petersen</span>
|
<span>Maja Petersen</span>
|
||||||
|
|
@ -200,7 +200,7 @@
|
||||||
</swp-data-table-cell>
|
</swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Maria Olsen">
|
<swp-data-table-row data-name="Maria Olsen" data-visits="2" data-created="2025-11" data-tags="ny" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar class="amber">MO</swp-avatar>
|
<swp-avatar class="amber">MO</swp-avatar>
|
||||||
<span>Maria Olsen</span>
|
<span>Maria Olsen</span>
|
||||||
|
|
@ -216,7 +216,7 @@
|
||||||
</swp-data-table-cell>
|
</swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Rikke Skov">
|
<swp-data-table-row data-name="Rikke Skov" data-visits="4" data-created="2025-08" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>RS</swp-avatar>
|
<swp-avatar>RS</swp-avatar>
|
||||||
<span>Rikke Skov</span>
|
<span>Rikke Skov</span>
|
||||||
|
|
@ -230,7 +230,7 @@
|
||||||
<swp-data-table-cell></swp-data-table-cell>
|
<swp-data-table-cell></swp-data-table-cell>
|
||||||
</swp-data-table-row>
|
</swp-data-table-row>
|
||||||
|
|
||||||
<swp-data-table-row data-name="Sofie Nielsen">
|
<swp-data-table-row data-name="Sofie Nielsen" data-visits="14" data-created="2024-03" data-tags="vip" data-drawer-trigger="customer-drawer">
|
||||||
<swp-data-table-cell>
|
<swp-data-table-cell>
|
||||||
<swp-avatar>SN</swp-avatar>
|
<swp-avatar>SN</swp-avatar>
|
||||||
<span>Sofie Nielsen</span>
|
<span>Sofie Nielsen</span>
|
||||||
|
|
@ -253,3 +253,160 @@
|
||||||
</swp-empty-state>
|
</swp-empty-state>
|
||||||
</swp-card>
|
</swp-card>
|
||||||
</swp-page-container>
|
</swp-page-container>
|
||||||
|
|
||||||
|
<!-- Customer Drawer -->
|
||||||
|
<div id="customer-drawer" data-drawer="xxl">
|
||||||
|
<swp-drawer-header>
|
||||||
|
<swp-drawer-title>
|
||||||
|
<i class="ph ph-user"></i>
|
||||||
|
<span localize="customers.drawer.title">Kundekort</span>
|
||||||
|
</swp-drawer-title>
|
||||||
|
<swp-drawer-close data-drawer-close>
|
||||||
|
<i class="ph ph-x"></i>
|
||||||
|
</swp-drawer-close>
|
||||||
|
</swp-drawer-header>
|
||||||
|
|
||||||
|
<swp-drawer-body>
|
||||||
|
<!-- Customer Header -->
|
||||||
|
<swp-customer-header>
|
||||||
|
<swp-customer-avatar-large id="drawerAvatar">SN</swp-customer-avatar-large>
|
||||||
|
<swp-customer-header-info>
|
||||||
|
<swp-customer-header-top>
|
||||||
|
<swp-customer-header-left>
|
||||||
|
<swp-customer-header-name id="drawerName">Sofie Nielsen</swp-customer-header-name>
|
||||||
|
<swp-customer-since id="drawerSince">Kunde siden marts 2024</swp-customer-since>
|
||||||
|
</swp-customer-header-left>
|
||||||
|
<swp-customer-header-contact>
|
||||||
|
<a href="tel:+4523456789" id="drawerPhoneLink">+45 23 45 67 89</a>
|
||||||
|
<a href="mailto:sofie@email.dk" id="drawerEmailLink">sofie@email.dk</a>
|
||||||
|
<swp-customer-tags id="drawerTags"></swp-customer-tags>
|
||||||
|
</swp-customer-header-contact>
|
||||||
|
</swp-customer-header-top>
|
||||||
|
</swp-customer-header-info>
|
||||||
|
</swp-customer-header>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<swp-quick-stats class="cols-3">
|
||||||
|
<swp-quick-stat>
|
||||||
|
<swp-stat-value id="drawerVisits">14</swp-stat-value>
|
||||||
|
<swp-stat-label localize="customers.drawer.visits">Besøg</swp-stat-label>
|
||||||
|
</swp-quick-stat>
|
||||||
|
<swp-quick-stat>
|
||||||
|
<swp-stat-value id="drawerInterval">32 dage</swp-stat-value>
|
||||||
|
<swp-stat-label localize="customers.drawer.avgInterval">Gns. interval</swp-stat-label>
|
||||||
|
</swp-quick-stat>
|
||||||
|
<swp-quick-stat>
|
||||||
|
<swp-stat-value id="drawerHairdresser">Emma L.</swp-stat-value>
|
||||||
|
<swp-stat-label localize="customers.drawer.preferredHairdresser">Foretrukken frisør</swp-stat-label>
|
||||||
|
</swp-quick-stat>
|
||||||
|
</swp-quick-stats>
|
||||||
|
|
||||||
|
<!-- Contact Info (Editable) -->
|
||||||
|
<div>
|
||||||
|
<swp-section-label localize="customers.drawer.contactInfo">Kontaktoplysninger</swp-section-label>
|
||||||
|
<swp-edit-section>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label localize="customers.drawer.phone">Telefon</swp-edit-label>
|
||||||
|
<swp-edit-input><input type="tel" id="editPhone" value="+45 23 45 67 89"></swp-edit-input>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label localize="customers.drawer.email">Email</swp-edit-label>
|
||||||
|
<swp-edit-input><input type="email" id="editEmail" value="sofie@email.dk"></swp-edit-input>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label localize="customers.drawer.address">Adresse</swp-edit-label>
|
||||||
|
<swp-edit-input><input type="text" id="editAddress" value="Hovedgaden 12"></swp-edit-input>
|
||||||
|
</swp-edit-row>
|
||||||
|
<swp-edit-row>
|
||||||
|
<swp-edit-label localize="customers.drawer.zipCity">Postnr + By</swp-edit-label>
|
||||||
|
<swp-edit-input>
|
||||||
|
<input type="text" id="editZip" value="2100" class="short">
|
||||||
|
<input type="text" id="editCity" value="København Ø">
|
||||||
|
</swp-edit-input>
|
||||||
|
</swp-edit-row>
|
||||||
|
</swp-edit-section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Marketing Opt-in -->
|
||||||
|
<div>
|
||||||
|
<swp-section-label localize="customers.drawer.marketing">Marketing</swp-section-label>
|
||||||
|
<swp-marketing-section>
|
||||||
|
<swp-toggle-row>
|
||||||
|
<swp-toggle-label localize="customers.drawer.emailMarketing">Email marketing</swp-toggle-label>
|
||||||
|
<swp-toggle-slider id="toggleEmail" data-value="yes">
|
||||||
|
<swp-toggle-option localize="common.yes">Ja</swp-toggle-option>
|
||||||
|
<swp-toggle-option localize="common.no">Nej</swp-toggle-option>
|
||||||
|
</swp-toggle-slider>
|
||||||
|
</swp-toggle-row>
|
||||||
|
<swp-toggle-row>
|
||||||
|
<swp-toggle-label localize="customers.drawer.smsMarketing">SMS marketing</swp-toggle-label>
|
||||||
|
<swp-toggle-slider id="toggleSms" data-value="no">
|
||||||
|
<swp-toggle-option localize="common.yes">Ja</swp-toggle-option>
|
||||||
|
<swp-toggle-option localize="common.no">Nej</swp-toggle-option>
|
||||||
|
</swp-toggle-slider>
|
||||||
|
</swp-toggle-row>
|
||||||
|
</swp-marketing-section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Profile Boxes -->
|
||||||
|
<div>
|
||||||
|
<swp-section-label localize="customers.drawer.profile">Profil</swp-section-label>
|
||||||
|
<swp-profile-boxes>
|
||||||
|
<swp-profile-box>
|
||||||
|
<swp-profile-box-label localize="customers.drawer.hairType">Hårtype</swp-profile-box-label>
|
||||||
|
<swp-profile-box-value>Medium · Bølget</swp-profile-box-value>
|
||||||
|
</swp-profile-box>
|
||||||
|
<swp-profile-box>
|
||||||
|
<swp-profile-box-label localize="customers.drawer.porosity">Porøsitet</swp-profile-box-label>
|
||||||
|
<swp-profile-box-value>Medium</swp-profile-box-value>
|
||||||
|
</swp-profile-box>
|
||||||
|
<swp-profile-box>
|
||||||
|
<swp-profile-box-label localize="customers.drawer.preference">Præference</swp-profile-box-label>
|
||||||
|
<swp-profile-box-value>Kold tone, ikke for mørk</swp-profile-box-value>
|
||||||
|
</swp-profile-box>
|
||||||
|
<swp-profile-box class="warning">
|
||||||
|
<swp-profile-box-label localize="customers.drawer.warnings">Advarsler</swp-profile-box-label>
|
||||||
|
<swp-profile-box-value>Parfume-allergi</swp-profile-box-value>
|
||||||
|
</swp-profile-box>
|
||||||
|
</swp-profile-boxes>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart -->
|
||||||
|
<swp-chart-section>
|
||||||
|
<swp-chart-header>
|
||||||
|
<swp-section-label style="margin-bottom: 0;" localize="customers.drawer.revenueChart">Omsætning (sidste 6 mdr)</swp-section-label>
|
||||||
|
<swp-chart-legend>
|
||||||
|
<swp-chart-legend-item>
|
||||||
|
<swp-chart-legend-dot class="services"></swp-chart-legend-dot>
|
||||||
|
<span localize="customers.drawer.services">Services</span>
|
||||||
|
</swp-chart-legend-item>
|
||||||
|
<swp-chart-legend-item>
|
||||||
|
<swp-chart-legend-dot class="products"></swp-chart-legend-dot>
|
||||||
|
<span localize="customers.drawer.products">Produkter</span>
|
||||||
|
</swp-chart-legend-item>
|
||||||
|
</swp-chart-legend>
|
||||||
|
</swp-chart-header>
|
||||||
|
<swp-chart-container id="customerChart"></swp-chart-container>
|
||||||
|
</swp-chart-section>
|
||||||
|
|
||||||
|
<!-- Notes -->
|
||||||
|
<swp-notes-section>
|
||||||
|
<swp-section-label localize="customers.drawer.recentNotes">Seneste noter</swp-section-label>
|
||||||
|
<swp-note-item>
|
||||||
|
<swp-note-meta>
|
||||||
|
<swp-note-type localize="customers.drawer.noteType">Note</swp-note-type>
|
||||||
|
<swp-note-date>9. dec 2025</swp-note-date>
|
||||||
|
</swp-note-meta>
|
||||||
|
<swp-note-text>Kunden foretrækker naturlige farver og ønsker lidt ekstra tid til konsultation.</swp-note-text>
|
||||||
|
</swp-note-item>
|
||||||
|
<swp-note-item>
|
||||||
|
<swp-note-meta>
|
||||||
|
<swp-note-type localize="customers.drawer.colorFormula">Farveformel</swp-note-type>
|
||||||
|
<swp-note-date>12. nov 2025</swp-note-date>
|
||||||
|
</swp-note-meta>
|
||||||
|
<swp-note-text>7/1 + 7/0 (1:1) · Oxidant 6% · Virketid 35 min</swp-note-text>
|
||||||
|
</swp-note-item>
|
||||||
|
<swp-see-all-link localize="customers.drawer.seeAllNotes">Se alle noter →</swp-see-all-link>
|
||||||
|
</swp-notes-section>
|
||||||
|
</swp-drawer-body>
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1039,6 +1039,8 @@ swp-form-input {
|
||||||
|
|
||||||
[data-drawer] swp-section-label {
|
[data-drawer] swp-section-label {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-drawer] swp-data-section {
|
[data-drawer] swp-data-section {
|
||||||
|
|
@ -1179,7 +1181,7 @@ swp-status-indicator {
|
||||||
&[data-active="false"] {
|
&[data-active="false"] {
|
||||||
background: var(--bg-red-medium);
|
background: var(--bg-red-medium);
|
||||||
color: var(--color-red);
|
color: var(--color-red);
|
||||||
border: 1px solid var(--bg-red-border);
|
border: 1px solid var(--border-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
* Feature-specific styling only.
|
* Feature-specific styling only.
|
||||||
* Reuses:
|
* Reuses:
|
||||||
* - swp-sticky-header, swp-header-content, swp-page-container (page.css)
|
* - swp-sticky-header, swp-header-content, swp-page-container (page.css)
|
||||||
* - swp-stats-row, swp-stat-card (stats.css)
|
* - swp-stats-row, swp-stat-card, swp-quick-stats (stats.css)
|
||||||
* - swp-action-bar, swp-search-input (components.css, services.css)
|
* - swp-action-bar, swp-search-input (components.css, services.css)
|
||||||
* - swp-data-table, swp-avatar, swp-tag, swp-empty-state (components.css)
|
* - swp-data-table, swp-avatar, swp-tag, swp-empty-state (components.css)
|
||||||
* - swp-btn (components.css)
|
* - swp-btn (components.css)
|
||||||
|
|
@ -66,3 +66,274 @@ swp-card.customers-list swp-data-table-cell:last-child {
|
||||||
gap: var(--spacing-2);
|
gap: var(--spacing-2);
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===========================================
|
||||||
|
CUSTOMER DRAWER
|
||||||
|
Reuses: swp-drawer-* (drawers.css), swp-section-label (components.css),
|
||||||
|
swp-edit-section/row (components.css), swp-toggle-row/slider (controls.css)
|
||||||
|
=========================================== */
|
||||||
|
|
||||||
|
/* Customer Header */
|
||||||
|
swp-customer-header {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-6);
|
||||||
|
padding-bottom: var(--spacing-6);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
margin-bottom: var(--spacing-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-avatar-large {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-teal);
|
||||||
|
color: white;
|
||||||
|
font-size: var(--font-size-3xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-header-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-header-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-header-name {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-since {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-header-contact {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-teal);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-customer-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
margin-top: var(--spacing-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Marketing Section */
|
||||||
|
swp-marketing-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
|
||||||
|
swp-toggle-row,
|
||||||
|
swp-toggle-row:last-child {
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile Boxes (2x2 grid) */
|
||||||
|
swp-profile-boxes {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-profile-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: var(--bg-red-subtle);
|
||||||
|
border: 1px solid var(--border-red);
|
||||||
|
|
||||||
|
swp-profile-box-label {
|
||||||
|
color: var(--color-red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-profile-box-label {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-profile-box-value {
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chart Section */
|
||||||
|
swp-chart-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
margin-top: var(--spacing-6);
|
||||||
|
padding-top: var(--spacing-6);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-chart-legend {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-chart-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-chart-legend-dot {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
|
||||||
|
&.services {
|
||||||
|
background: var(--color-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.products {
|
||||||
|
background: var(--color-blue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-chart-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notes Section */
|
||||||
|
swp-notes-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-4);
|
||||||
|
margin-top: var(--spacing-6);
|
||||||
|
padding-top: var(--spacing-6);
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-note-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
padding: var(--spacing-4);
|
||||||
|
background: var(--color-background-alt);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-note-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-note-type {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--color-teal);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-note-date {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-note-text {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-text);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-see-all-link {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--color-teal);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
padding-top: var(--spacing-2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit input variant for drawer */
|
||||||
|
swp-edit-input {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-2);
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-3) var(--spacing-4);
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--color-surface);
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--color-teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.short {
|
||||||
|
width: 80px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
[data-drawer="md"] { --drawer-width: 360px; }
|
[data-drawer="md"] { --drawer-width: 360px; }
|
||||||
[data-drawer="lg"] { --drawer-width: 420px; }
|
[data-drawer="lg"] { --drawer-width: 420px; }
|
||||||
[data-drawer="xl"] { --drawer-width: 480px; }
|
[data-drawer="xl"] { --drawer-width: 480px; }
|
||||||
|
[data-drawer="xxl"] { --drawer-width: 680px; }
|
||||||
|
|
||||||
/* Legacy support for existing drawers */
|
/* Legacy support for existing drawers */
|
||||||
swp-profile-drawer,
|
swp-profile-drawer,
|
||||||
|
|
|
||||||
|
|
@ -286,7 +286,7 @@ swp-employee-status {
|
||||||
&[data-active="false"] {
|
&[data-active="false"] {
|
||||||
background: var(--bg-red-medium);
|
background: var(--bg-red-medium);
|
||||||
color: var(--color-red);
|
color: var(--color-red);
|
||||||
border: 1px solid var(--bg-red-border);
|
border: 1px solid var(--border-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,10 @@ swp-quick-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: var(--card-gap);
|
gap: var(--card-gap);
|
||||||
|
|
||||||
|
&.cols-3 {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-quick-stat {
|
swp-quick-stat {
|
||||||
|
|
@ -235,6 +239,7 @@ swp-quick-stat {
|
||||||
padding: var(--card-padding);
|
padding: var(--card-padding);
|
||||||
background: var(--color-background-alt);
|
background: var(--color-background-alt);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
swp-quick-stat swp-stat-value {
|
swp-quick-stat swp-stat-value {
|
||||||
|
|
@ -249,6 +254,11 @@ swp-quick-stat swp-stat-label {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
swp-quick-stat.highlight {
|
||||||
|
background: var(--bg-teal-subtle);
|
||||||
|
border: 1px solid var(--bg-teal-border);
|
||||||
|
}
|
||||||
|
|
||||||
/* ===========================================
|
/* ===========================================
|
||||||
RESPONSIVE
|
RESPONSIVE
|
||||||
=========================================== */
|
=========================================== */
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { CashController } from './modules/cash';
|
||||||
import { EmployeesController } from './modules/employees';
|
import { EmployeesController } from './modules/employees';
|
||||||
import { ControlsController } from './modules/controls';
|
import { ControlsController } from './modules/controls';
|
||||||
import { ServicesController } from './modules/services';
|
import { ServicesController } from './modules/services';
|
||||||
|
import { CustomersController } from './modules/customers';
|
||||||
import { TrackingController } from './modules/tracking';
|
import { TrackingController } from './modules/tracking';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,6 +29,7 @@ export class App {
|
||||||
readonly employees: EmployeesController;
|
readonly employees: EmployeesController;
|
||||||
readonly controls: ControlsController;
|
readonly controls: ControlsController;
|
||||||
readonly services: ServicesController;
|
readonly services: ServicesController;
|
||||||
|
readonly customers: CustomersController;
|
||||||
readonly tracking: TrackingController;
|
readonly tracking: TrackingController;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -41,6 +43,7 @@ export class App {
|
||||||
this.employees = new EmployeesController();
|
this.employees = new EmployeesController();
|
||||||
this.controls = new ControlsController();
|
this.controls = new ControlsController();
|
||||||
this.services = new ServicesController();
|
this.services = new ServicesController();
|
||||||
|
this.customers = new CustomersController();
|
||||||
this.tracking = new TrackingController();
|
this.tracking = new TrackingController();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
235
PlanTempus.Application/wwwroot/ts/modules/customers.ts
Normal file
235
PlanTempus.Application/wwwroot/ts/modules/customers.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
/**
|
||||||
|
* Customers Controller
|
||||||
|
*
|
||||||
|
* Handles:
|
||||||
|
* - Fuzzy search with Fuse.js
|
||||||
|
* - Customer drawer population
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
interface CustomerItem {
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
email: string;
|
||||||
|
visits: string;
|
||||||
|
created: string;
|
||||||
|
tags: string;
|
||||||
|
hairdresser: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CustomersController {
|
||||||
|
private fuse: Fuse<CustomerItem> | null = null;
|
||||||
|
private customers: CustomerItem[] = [];
|
||||||
|
private searchInput: HTMLInputElement | null = null;
|
||||||
|
private emptyState: HTMLElement | null = null;
|
||||||
|
private dataTable: HTMLElement | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Only initialize if we're on the customers page
|
||||||
|
const customersTable = document.querySelector('swp-card.customers-list');
|
||||||
|
if (!customersTable) return;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private init(): void {
|
||||||
|
this.searchInput = document.getElementById('searchInput') as HTMLInputElement;
|
||||||
|
this.emptyState = document.getElementById('emptyState');
|
||||||
|
this.dataTable = document.querySelector('swp-card.customers-list swp-data-table');
|
||||||
|
|
||||||
|
this.buildCustomerIndex();
|
||||||
|
this.setupSearch();
|
||||||
|
this.setupDrawerPopulation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCustomerIndex(): void {
|
||||||
|
const customerRows = document.querySelectorAll('swp-card.customers-list swp-data-table-row');
|
||||||
|
|
||||||
|
customerRows.forEach((row) => {
|
||||||
|
const element = row as HTMLElement;
|
||||||
|
const cells = element.querySelectorAll('swp-data-table-cell');
|
||||||
|
|
||||||
|
const name = element.dataset.name || '';
|
||||||
|
const phone = cells[1]?.textContent?.trim() || '';
|
||||||
|
const email = cells[2]?.textContent?.trim() || '';
|
||||||
|
const visits = element.dataset.visits || '';
|
||||||
|
const created = element.dataset.created || '';
|
||||||
|
const tags = element.dataset.tags || '';
|
||||||
|
const hairdresser = cells[5]?.textContent?.trim() || '';
|
||||||
|
|
||||||
|
this.customers.push({
|
||||||
|
name,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
|
visits,
|
||||||
|
created,
|
||||||
|
tags,
|
||||||
|
hairdresser,
|
||||||
|
element
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSearch(): void {
|
||||||
|
if (!this.searchInput) return;
|
||||||
|
|
||||||
|
// Initialize Fuse.js with multiple search keys
|
||||||
|
this.fuse = new Fuse(this.customers, {
|
||||||
|
keys: ['name', 'phone', 'email'],
|
||||||
|
threshold: 0.3,
|
||||||
|
minMatchCharLength: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for input with debounce
|
||||||
|
let debounceTimer: number;
|
||||||
|
this.searchInput.addEventListener('input', (e) => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = window.setTimeout(() => {
|
||||||
|
const query = (e.target as HTMLInputElement).value.trim();
|
||||||
|
this.filterCustomers(query);
|
||||||
|
}, 150);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterCustomers(query: string): void {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
this.showAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.fuse) return;
|
||||||
|
|
||||||
|
// Get matching customers
|
||||||
|
const results = this.fuse.search(query);
|
||||||
|
const matchingCustomers = new Set(results.map(r => r.item.element));
|
||||||
|
|
||||||
|
let visibleCount = 0;
|
||||||
|
|
||||||
|
// Show/hide customers
|
||||||
|
this.customers.forEach(customer => {
|
||||||
|
if (matchingCustomers.has(customer.element)) {
|
||||||
|
customer.element.style.display = 'grid';
|
||||||
|
visibleCount++;
|
||||||
|
} else {
|
||||||
|
customer.element.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide empty state
|
||||||
|
this.updateEmptyState(visibleCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private showAll(): void {
|
||||||
|
this.customers.forEach(customer => {
|
||||||
|
customer.element.style.display = 'grid';
|
||||||
|
});
|
||||||
|
this.updateEmptyState(this.customers.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateEmptyState(visibleCount: number): void {
|
||||||
|
if (!this.emptyState || !this.dataTable) return;
|
||||||
|
|
||||||
|
if (visibleCount === 0) {
|
||||||
|
this.emptyState.style.display = 'flex';
|
||||||
|
// Hide header when no results
|
||||||
|
const header = this.dataTable.querySelector('swp-data-table-header') as HTMLElement;
|
||||||
|
if (header) header.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
this.emptyState.style.display = 'none';
|
||||||
|
// Show header when results exist
|
||||||
|
const header = this.dataTable.querySelector('swp-data-table-header') as HTMLElement;
|
||||||
|
if (header) header.style.display = 'grid';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupDrawerPopulation(): void {
|
||||||
|
// Listen for clicks on customer rows to populate drawer
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const row = (e.target as HTMLElement).closest<HTMLElement>('swp-data-table-row[data-drawer-trigger="customer-drawer"]');
|
||||||
|
if (!row) return;
|
||||||
|
|
||||||
|
this.populateDrawer(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private populateDrawer(row: HTMLElement): void {
|
||||||
|
const cells = row.querySelectorAll('swp-data-table-cell');
|
||||||
|
|
||||||
|
const name = row.dataset.name || '';
|
||||||
|
const phone = cells[1]?.textContent?.trim() || '';
|
||||||
|
const email = cells[2]?.textContent?.trim() || '';
|
||||||
|
const visits = row.dataset.visits || '';
|
||||||
|
const created = row.dataset.created || '';
|
||||||
|
const tags = row.dataset.tags || '';
|
||||||
|
const hairdresser = cells[5]?.textContent?.trim() || '';
|
||||||
|
|
||||||
|
// Generate initials
|
||||||
|
const initials = name.split(' ').map(n => n[0]).join('').toUpperCase();
|
||||||
|
|
||||||
|
// Format "Kunde siden"
|
||||||
|
const createdDate = created ? this.formatCreatedDate(created) : 'Ukendt';
|
||||||
|
|
||||||
|
// Update drawer elements
|
||||||
|
const drawerAvatar = document.getElementById('drawerAvatar');
|
||||||
|
const drawerName = document.getElementById('drawerName');
|
||||||
|
const drawerSince = document.getElementById('drawerSince');
|
||||||
|
const drawerPhoneLink = document.getElementById('drawerPhoneLink') as HTMLAnchorElement;
|
||||||
|
const drawerEmailLink = document.getElementById('drawerEmailLink') as HTMLAnchorElement;
|
||||||
|
const drawerVisits = document.getElementById('drawerVisits');
|
||||||
|
const drawerHairdresser = document.getElementById('drawerHairdresser');
|
||||||
|
const drawerTags = document.getElementById('drawerTags');
|
||||||
|
const editPhone = document.getElementById('editPhone') as HTMLInputElement;
|
||||||
|
const editEmail = document.getElementById('editEmail') as HTMLInputElement;
|
||||||
|
|
||||||
|
if (drawerAvatar) drawerAvatar.textContent = initials;
|
||||||
|
if (drawerName) drawerName.textContent = name;
|
||||||
|
if (drawerSince) drawerSince.textContent = `Kunde siden ${createdDate}`;
|
||||||
|
|
||||||
|
if (drawerPhoneLink) {
|
||||||
|
drawerPhoneLink.textContent = phone;
|
||||||
|
drawerPhoneLink.href = `tel:${phone.replace(/\s/g, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawerEmailLink) {
|
||||||
|
drawerEmailLink.textContent = email;
|
||||||
|
drawerEmailLink.href = `mailto:${email}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawerVisits) drawerVisits.textContent = visits;
|
||||||
|
if (drawerHairdresser) drawerHairdresser.textContent = hairdresser;
|
||||||
|
|
||||||
|
// Update editable fields
|
||||||
|
if (editPhone) editPhone.value = phone;
|
||||||
|
if (editEmail) editEmail.value = email;
|
||||||
|
|
||||||
|
// Update tags
|
||||||
|
if (drawerTags) {
|
||||||
|
drawerTags.innerHTML = '';
|
||||||
|
if (tags) {
|
||||||
|
tags.split(',').forEach(tag => {
|
||||||
|
const tagEl = document.createElement('swp-tag');
|
||||||
|
tagEl.className = tag.trim();
|
||||||
|
tagEl.textContent = this.formatTagLabel(tag.trim());
|
||||||
|
drawerTags.appendChild(tagEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatCreatedDate(dateStr: string): string {
|
||||||
|
const months = [
|
||||||
|
'januar', 'februar', 'marts', 'april', 'maj', 'juni',
|
||||||
|
'juli', 'august', 'september', 'oktober', 'november', 'december'
|
||||||
|
];
|
||||||
|
const [year, month] = dateStr.split('-');
|
||||||
|
const monthIndex = parseInt(month, 10) - 1;
|
||||||
|
return `${months[monthIndex]} ${year}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatTagLabel(tag: string): string {
|
||||||
|
// Capitalize first letter
|
||||||
|
return tag.charAt(0).toUpperCase() + tag.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue