/** * Customers Controller * * Handles: * - Fuzzy search with Fuse.js * - Customer drawer population * - Customer detail economy chart */ import Fuse from 'fuse.js'; import { createChart } from '@sevenweirdpeople/swp-charting'; interface CustomerItem { name: string; phone: string; email: string; visits: string; created: string; tags: string; hairdresser: string; element: HTMLElement; } export class CustomersController { private fuse: Fuse | 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('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); } } /** * Customer Economy Controller * * Handles the economy chart on customer detail page. * Initializes chart lazily when economy tab is shown. */ interface ChartDataPoint { x: string; y: number; } interface ChartSeries { name: string; color: string; data: ChartDataPoint[]; } interface CustomerChartData { categories: string[]; series: ChartSeries[]; } class CustomerEconomyController { private chartInitialized = false; private chart: ReturnType | null = null; constructor() { this.setupTabListener(); // Check if economy tab is already active on page load this.checkInitialTab(); } private setupTabListener(): void { document.addEventListener('click', (e: Event) => { const target = e.target as HTMLElement; const tab = target.closest('swp-tab[data-tab="economy"]'); if (tab) { // Small delay to let tab content become visible setTimeout(() => this.initializeChart(), 50); } }); } private checkInitialTab(): void { const activeTab = document.querySelector('swp-tab[data-tab="economy"].active'); if (activeTab) { this.initializeChart(); } } private initializeChart(): void { if (this.chartInitialized) return; const container = document.getElementById('customerRevenueChart'); if (!container) return; const dataScript = document.getElementById('customerRevenueChartData'); if (!dataScript) return; try { const data = JSON.parse(dataScript.textContent || '') as CustomerChartData; this.createRevenueChart(container, data); this.chartInitialized = true; } catch (err) { console.error('Failed to parse chart data:', err); } } private createRevenueChart(container: HTMLElement, data: CustomerChartData): void { this.chart = createChart(container, { deferRender: true, height: 200, xAxis: { categories: data.categories }, series: data.series.map(s => ({ name: s.name, color: s.color, data: s.data })), legend: false }); } } // Initialize economy controller if on customer detail page if (document.getElementById('customerRevenueChart') || document.querySelector('swp-tab[data-tab="economy"]')) { new CustomerEconomyController(); }