236 lines
7.3 KiB
TypeScript
236 lines
7.3 KiB
TypeScript
|
|
/**
|
||
|
|
* 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);
|
||
|
|
}
|
||
|
|
}
|