/** * TASKMATE - Utility Functions * ============================ */ // Date Formatting export function formatDate(dateString, options = {}) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); if (options.relative) { const diff = date - now; const days = Math.ceil(diff / (1000 * 60 * 60 * 24)); if (days === 0) return 'Heute'; if (days === 1) return 'Morgen'; if (days === -1) return 'Gestern'; if (days > 0 && days <= 7) return `In ${days} Tagen`; if (days < 0 && days >= -7) return `Vor ${Math.abs(days)} Tagen`; } return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: options.year ? 'numeric' : undefined, ...options }); } export function formatDateTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } export function formatTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); } export function formatRelativeTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diff = now - date; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return 'Gerade eben'; if (minutes < 60) return `Vor ${minutes} Min.`; if (hours < 24) return `Vor ${hours} Std.`; if (days < 7) return `Vor ${days} Tagen`; return formatDate(dateString, { year: true }); } // Time Duration export function formatDuration(hours, minutes) { const parts = []; if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); return parts.join(' ') || '0m'; } export function parseDuration(durationString) { const match = durationString.match(/(\d+)h\s*(\d+)?m?|(\d+)m/); if (!match) return { hours: 0, minutes: 0 }; if (match[1]) { return { hours: parseInt(match[1]) || 0, minutes: parseInt(match[2]) || 0 }; } return { hours: 0, minutes: parseInt(match[3]) || 0 }; } // File Size export function formatFileSize(bytes) { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; const k = 1024; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + units[i]; } // String Utilities export function truncate(str, length = 50) { if (!str || str.length <= length) return str; return str.substring(0, length) + '...'; } export function escapeHtml(str) { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } export function slugify(str) { return str .toLowerCase() .replace(/[äöüß]/g, match => ({ 'ä': 'ae', 'ö': 'oe', 'ü': 'ue', 'ß': 'ss' }[match])) .replace(/[^\w\s-]/g, '') .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); } export function getInitials(name) { if (!name) return '?'; return name .split(' ') .map(part => part.charAt(0).toUpperCase()) .slice(0, 2) .join(''); } // Color Utilities export function hexToRgba(hex, alpha = 1) { const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); if (!result) return hex; return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})`; } export function getContrastColor(hexColor) { const rgb = parseInt(hexColor.slice(1), 16); const r = (rgb >> 16) & 0xff; const g = (rgb >> 8) & 0xff; const b = (rgb >> 0) & 0xff; const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255; return luminance > 0.5 ? '#000000' : '#FFFFFF'; } // DOM Utilities export function $(selector, context = document) { return context.querySelector(selector); } export function $$(selector, context = document) { return Array.from(context.querySelectorAll(selector)); } export function createElement(tag, attributes = {}, children = []) { const element = document.createElement(tag); Object.entries(attributes).forEach(([key, value]) => { if (key === 'className') { element.className = value; } else if (key === 'dataset') { Object.entries(value).forEach(([dataKey, dataValue]) => { element.dataset[dataKey] = dataValue; }); } else if (key === 'style' && typeof value === 'object') { Object.assign(element.style, value); } else if (key.startsWith('on') && typeof value === 'function') { element.addEventListener(key.slice(2).toLowerCase(), value); } else if (key === 'checked' || key === 'disabled' || key === 'selected') { // Boolean properties must be set as properties, not attributes element[key] = value; } else { element.setAttribute(key, value); } }); children.forEach(child => { if (typeof child === 'string') { element.appendChild(document.createTextNode(child)); } else if (child instanceof Node) { element.appendChild(child); } }); return element; } export function removeElement(element) { if (element && element.parentNode) { element.parentNode.removeChild(element); } } export function clearElement(element) { while (element.firstChild) { element.removeChild(element.firstChild); } } // Event Utilities export function debounce(func, wait = 300) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } export function throttle(func, limit = 100) { let inThrottle; return function executedFunction(...args) { if (!inThrottle) { func(...args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; } export function onClickOutside(element, callback) { const handler = (event) => { if (!element.contains(event.target)) { callback(event); } }; document.addEventListener('click', handler); return () => document.removeEventListener('click', handler); } // Array Utilities export function moveInArray(array, fromIndex, toIndex) { const element = array[fromIndex]; array.splice(fromIndex, 1); array.splice(toIndex, 0, element); return array; } export function groupBy(array, key) { return array.reduce((groups, item) => { const value = typeof key === 'function' ? key(item) : item[key]; (groups[value] = groups[value] || []).push(item); return groups; }, {}); } export function sortBy(array, key, direction = 'asc') { const modifier = direction === 'desc' ? -1 : 1; return [...array].sort((a, b) => { const aValue = typeof key === 'function' ? key(a) : a[key]; const bValue = typeof key === 'function' ? key(b) : b[key]; if (aValue < bValue) return -1 * modifier; if (aValue > bValue) return 1 * modifier; return 0; }); } // Object Utilities export function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } export function deepMerge(target, source) { const output = { ...target }; Object.keys(source).forEach(key => { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (target[key] && typeof target[key] === 'object') { output[key] = deepMerge(target[key], source[key]); } else { output[key] = { ...source[key] }; } } else { output[key] = source[key]; } }); return output; } // URL Utilities export function getUrlDomain(url) { try { return new URL(url).hostname; } catch { return url; } } export function isValidUrl(string) { try { new URL(string); return true; } catch { return false; } } export function getFileExtension(filename) { return filename.split('.').pop().toLowerCase(); } export function isImageFile(filename) { const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp']; return imageExtensions.includes(getFileExtension(filename)); } // ID Generation export function generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } export function generateTempId() { return 'temp_' + generateId(); } // Local Storage export function getFromStorage(key, defaultValue = null) { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch { return defaultValue; } } export function setToStorage(key, value) { try { localStorage.setItem(key, JSON.stringify(value)); return true; } catch { return false; } } export function removeFromStorage(key) { try { localStorage.removeItem(key); return true; } catch { return false; } } // Priority Stars export function getPriorityStars(priority) { const config = { high: { stars: '★★★', label: 'Hohe Priorität', colorClass: 'priority-high' }, medium: { stars: '★★', label: 'Mittlere Priorität', colorClass: 'priority-medium' }, low: { stars: '★', label: 'Niedrige Priorität', colorClass: 'priority-low' } }; return config[priority] || config.medium; } export function createPriorityElement(priority) { const config = getPriorityStars(priority); const span = document.createElement('span'); span.className = `priority-stars ${config.colorClass}`; span.textContent = config.stars; span.title = config.label; return span; } // Due Date Status export function getDueDateStatus(dueDate) { if (!dueDate) return null; const due = new Date(dueDate); const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const dueDay = new Date(due.getFullYear(), due.getMonth(), due.getDate()); const diffDays = Math.ceil((dueDay - today) / (1000 * 60 * 60 * 24)); if (diffDays < 0) return 'overdue'; if (diffDays === 0) return 'today'; if (diffDays <= 2) return 'soon'; return 'normal'; } // Progress Calculation export function calculateProgress(subtasks) { if (!subtasks || subtasks.length === 0) return null; const completed = subtasks.filter(st => st.completed).length; return { completed, total: subtasks.length, percentage: Math.round((completed / subtasks.length) * 100) }; } // Search/Filter Helpers export function matchesSearch(text, query) { if (!query) return true; if (!text) return false; return text.toLowerCase().includes(query.toLowerCase()); } /** * Deep search in task content - searches in title, description, subtasks, and labels * @param {Object} task - The task object to search in * @param {string} query - The search query * @returns {boolean} - True if query matches any content */ export function searchInTaskContent(task, query) { if (!query || !query.trim()) return true; const searchQuery = query.toLowerCase().trim(); // Search in title if (task.title && task.title.toLowerCase().includes(searchQuery)) { return true; } // Search in description if (task.description && task.description.toLowerCase().includes(searchQuery)) { return true; } // Search in subtasks if (task.subtasks && Array.isArray(task.subtasks)) { for (const subtask of task.subtasks) { if (subtask.title && subtask.title.toLowerCase().includes(searchQuery)) { return true; } } } // Search in labels if (task.labels && Array.isArray(task.labels)) { for (const label of task.labels) { if (label.name && label.name.toLowerCase().includes(searchQuery)) { return true; } } } // Search in assigned user name if (task.assignedName && task.assignedName.toLowerCase().includes(searchQuery)) { return true; } return false; } export function filterTasks(tasks, filters, searchResultIds = [], columns = []) { // Hilfsfunktion: Prüfen ob Aufgabe in der letzten Spalte (erledigt) ist const isTaskCompleted = (task) => { if (!columns || columns.length === 0) return false; const lastColumnId = columns[columns.length - 1].id; return task.columnId === lastColumnId; }; return tasks.filter(task => { // Search query - use deep search // But allow tasks that were found by server search (for deep content like attachments) if (filters.search) { const isServerResult = searchResultIds.includes(task.id); const matchesClient = searchInTaskContent(task, filters.search); if (!isServerResult && !matchesClient) { return false; } } // Priority filter if (filters.priority && filters.priority !== 'all') { if (task.priority !== filters.priority) return false; } // Assignee filter if (filters.assignee && filters.assignee !== 'all') { if (task.assignedTo !== parseInt(filters.assignee)) return false; } // Label filter if (filters.label && filters.label !== 'all') { const hasLabel = task.labels?.some(l => l.id === parseInt(filters.label)); if (!hasLabel) return false; } // Due date filter if (filters.dueDate && filters.dueDate !== 'all' && filters.dueDate !== '') { const status = getDueDateStatus(task.dueDate); // Bei "überfällig" erledigte Aufgaben ausschließen if (filters.dueDate === 'overdue') { if (status !== 'overdue' || isTaskCompleted(task)) return false; } if (filters.dueDate === 'today' && status !== 'today') return false; if (filters.dueDate === 'week') { const due = new Date(task.dueDate); const weekFromNow = new Date(); weekFromNow.setDate(weekFromNow.getDate() + 7); if (!task.dueDate || due > weekFromNow) return false; } } return true; }); } // Clipboard export async function copyToClipboard(text) { try { await navigator.clipboard.writeText(text); return true; } catch { // Fallback const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); try { document.execCommand('copy'); return true; } finally { document.body.removeChild(textarea); } } } // Keyboard Helpers export function getKeyCombo(event) { const parts = []; if (event.ctrlKey || event.metaKey) parts.push('Ctrl'); if (event.altKey) parts.push('Alt'); if (event.shiftKey) parts.push('Shift'); if (event.key && !['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) { parts.push(event.key.toUpperCase()); } return parts.join('+'); } // Focus Management export function trapFocus(element) { const focusableElements = element.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstFocusable = focusableElements[0]; const lastFocusable = focusableElements[focusableElements.length - 1]; const handleKeydown = (e) => { if (e.key !== 'Tab') return; if (e.shiftKey) { if (document.activeElement === firstFocusable) { lastFocusable.focus(); e.preventDefault(); } } else { if (document.activeElement === lastFocusable) { firstFocusable.focus(); e.preventDefault(); } } }; element.addEventListener('keydown', handleKeydown); return () => element.removeEventListener('keydown', handleKeydown); } // Announcements for Screen Readers export function announce(message, priority = 'polite') { const announcer = document.getElementById('sr-announcer') || createAnnouncer(); announcer.setAttribute('aria-live', priority); announcer.textContent = message; } function createAnnouncer() { const announcer = document.createElement('div'); announcer.id = 'sr-announcer'; announcer.setAttribute('aria-live', 'polite'); announcer.setAttribute('aria-atomic', 'true'); announcer.className = 'sr-only'; document.body.appendChild(announcer); return announcer; }