Dieser Commit ist enthalten in:
Claude Project Manager
2025-12-28 21:36:45 +00:00
Commit ab1e5be9a9
146 geänderte Dateien mit 65525 neuen und 0 gelöschten Zeilen

615
frontend/js/utils.js Normale Datei
Datei anzeigen

@ -0,0 +1,615 @@
/**
* 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;
}