UI-Redesign: AegisSight Design, Filter-Popover, Header-Umbau
- Session-Timeout auf 60 Minuten erhöht (ACCESS_TOKEN_EXPIRY + SESSION_TIMEOUT) - AegisSight Light Theme: Gold-Akzent (#C8A851) statt Indigo - Navigation-Tabs in eigene Zeile unter Header verschoben (HTML-Struktur) - Filter-Bar durch kompaktes Popover mit Checkboxen ersetzt (Mehrfachauswahl) - Archiv-Funktion repariert (lädt jetzt per API statt leerem Store) - Filter-Bugs behoben: Reset-Button ID, Default-Werte, Ohne-Datum-Filter - Mehrspalten-Layout Feature entfernt - Online-Status vom Header an User-Avatar verschoben (grüner Punkt) - Lupen-Icon entfernt - CLAUDE.md: Docker-Deploy und CSS-Tricks Regeln aktualisiert Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -570,8 +570,10 @@ class ApiClient {
|
||||
return this.put(`/tasks/${taskId}/archive`, { archived: true });
|
||||
}
|
||||
|
||||
async restoreTask(projectId, taskId) {
|
||||
return this.put(`/tasks/${taskId}/archive`, { archived: false });
|
||||
async restoreTask(projectId, taskId, columnId = null) {
|
||||
const data = { archived: false };
|
||||
if (columnId) data.columnId = columnId;
|
||||
return this.put(`/tasks/${taskId}/archive`, data);
|
||||
}
|
||||
|
||||
async getTaskHistory(projectId, taskId) {
|
||||
|
||||
@@ -263,27 +263,33 @@ class App {
|
||||
// Search - Hybrid search with client-side filtering and server search
|
||||
this.setupSearch();
|
||||
|
||||
// Filter changes
|
||||
$('#filter-priority')?.addEventListener('change', (e) => {
|
||||
store.setFilter('priority', e.target.value);
|
||||
// Filter toggle popover
|
||||
$('#btn-filter-toggle')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
$('#filter-popover')?.classList.toggle('hidden');
|
||||
});
|
||||
|
||||
$('#filter-assignee')?.addEventListener('change', (e) => {
|
||||
store.setFilter('assignee', e.target.value);
|
||||
// Close popover on click outside
|
||||
document.addEventListener('click', (e) => {
|
||||
const popover = $('#filter-popover');
|
||||
const toggleBtn = $('#btn-filter-toggle');
|
||||
if (popover && !popover.contains(e.target) && !toggleBtn?.contains(e.target)) {
|
||||
popover.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
$('#filter-labels')?.addEventListener('change', (e) => {
|
||||
store.setFilter('label', e.target.value);
|
||||
});
|
||||
|
||||
$('#filter-due')?.addEventListener('change', (e) => {
|
||||
store.setFilter('dueDate', e.target.value);
|
||||
});
|
||||
// Filter changes - checkbox listeners for static filters
|
||||
$('#filter-priority-list')?.addEventListener('change', () => this.applyCheckboxFilters());
|
||||
$('#filter-due-list')?.addEventListener('change', () => this.applyCheckboxFilters());
|
||||
$('#filter-assignee-list')?.addEventListener('change', () => this.applyCheckboxFilters());
|
||||
$('#filter-labels-list')?.addEventListener('change', () => this.applyCheckboxFilters());
|
||||
|
||||
// Reset filters
|
||||
$('#btn-reset-filters')?.addEventListener('click', () => {
|
||||
$('#btn-clear-filters')?.addEventListener('click', () => {
|
||||
store.resetFilters();
|
||||
this.resetFilterInputs();
|
||||
this.updateFilterButtonState();
|
||||
$('#filter-popover')?.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Open archive modal
|
||||
@@ -301,6 +307,9 @@ class App {
|
||||
// Confirm dialog events
|
||||
window.addEventListener('confirm:show', (e) => this.showConfirmDialog(e.detail));
|
||||
|
||||
// Column select dialog events
|
||||
window.addEventListener('column-select:show', (e) => this.showColumnSelectDialog(e.detail));
|
||||
|
||||
// Lightbox events
|
||||
window.addEventListener('lightbox:open', (e) => this.openLightbox(e.detail));
|
||||
|
||||
@@ -702,11 +711,13 @@ class App {
|
||||
// Initialize contacts view when switching to it
|
||||
if (view === 'contacts') {
|
||||
window.initContactsPromise = window.initContactsPromise || import('./contacts.js').then(module => {
|
||||
if (module.initContacts) {
|
||||
return module.initContacts();
|
||||
if (module.contactsManager && !module.contactsManager.isInitialized) {
|
||||
return module.contactsManager.init();
|
||||
}
|
||||
});
|
||||
window.initContactsPromise.catch(console.error);
|
||||
window.initContactsPromise.catch(error => {
|
||||
console.error('[App] Contacts module error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Render list view when switching to it (delayed to ensure store is updated)
|
||||
@@ -752,65 +763,114 @@ class App {
|
||||
}
|
||||
|
||||
populateAssigneeFilter() {
|
||||
const select = $('#filter-assignee');
|
||||
if (!select) return;
|
||||
const list = $('#filter-assignee-list');
|
||||
if (!list) return;
|
||||
|
||||
const users = store.get('users');
|
||||
select.innerHTML = '<option value="all">Alle Bearbeiter</option>';
|
||||
list.innerHTML = '';
|
||||
|
||||
users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.id;
|
||||
option.textContent = user.displayName || user.email || 'Unbekannt';
|
||||
select.appendChild(option);
|
||||
const label = document.createElement('label');
|
||||
label.className = 'filter-checkbox-item';
|
||||
label.innerHTML = `<input type="checkbox" name="filter-assignee" value="${user.id}"><span>${user.displayName || user.email || 'Unbekannt'}</span>`;
|
||||
list.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
populateLabelFilter() {
|
||||
const select = $('#filter-labels');
|
||||
if (!select) return;
|
||||
const list = $('#filter-labels-list');
|
||||
if (!list) return;
|
||||
|
||||
const labels = store.get('labels');
|
||||
select.innerHTML = '<option value="all">Alle Labels</option>';
|
||||
list.innerHTML = '';
|
||||
|
||||
labels.forEach(label => {
|
||||
const option = document.createElement('option');
|
||||
option.value = label.id;
|
||||
option.textContent = label.name;
|
||||
select.appendChild(option);
|
||||
const item = document.createElement('label');
|
||||
item.className = 'filter-checkbox-item';
|
||||
item.innerHTML = `<input type="checkbox" name="filter-labels" value="${label.id}"><span>${label.name}</span>`;
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
applyCheckboxFilters() {
|
||||
const getCheckedValues = (name) => {
|
||||
const checkboxes = document.querySelectorAll(`input[name="${name}"]:checked`);
|
||||
return Array.from(checkboxes).map(cb => cb.value);
|
||||
};
|
||||
|
||||
const priority = getCheckedValues('filter-priority');
|
||||
const assignee = getCheckedValues('filter-assignee');
|
||||
const label = getCheckedValues('filter-labels');
|
||||
const dueDate = getCheckedValues('filter-due');
|
||||
|
||||
store.setFilter('priority', priority.length > 0 ? priority : '');
|
||||
store.setFilter('assignee', assignee.length > 0 ? assignee : '');
|
||||
store.setFilter('label', label.length > 0 ? label : '');
|
||||
store.setFilter('dueDate', dueDate.length > 0 ? dueDate : '');
|
||||
|
||||
this.updateFilterButtonState();
|
||||
}
|
||||
|
||||
resetFilterInputs() {
|
||||
const priority = $('#filter-priority');
|
||||
const assignee = $('#filter-assignee');
|
||||
const labels = $('#filter-labels');
|
||||
const dueDate = $('#filter-due');
|
||||
const checkboxes = document.querySelectorAll('#filter-popover input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => cb.checked = false);
|
||||
|
||||
const search = $('#search-input');
|
||||
const searchClear = $('#search-clear');
|
||||
const searchContainer = $('.search-container');
|
||||
|
||||
if (priority) priority.value = 'all';
|
||||
if (assignee) assignee.value = 'all';
|
||||
if (labels) labels.value = 'all';
|
||||
if (dueDate) dueDate.value = '';
|
||||
if (search) search.value = '';
|
||||
if (searchClear) searchClear.classList.add('hidden');
|
||||
if (searchContainer) searchContainer.classList.remove('has-search');
|
||||
}
|
||||
|
||||
openArchiveModal() {
|
||||
this.handleModalOpen({ modalId: 'archive-modal' });
|
||||
this.renderArchiveList();
|
||||
updateFilterButtonState() {
|
||||
const btn = $('#btn-filter-toggle');
|
||||
if (!btn) return;
|
||||
|
||||
const groups = ['filter-priority', 'filter-assignee', 'filter-labels', 'filter-due'];
|
||||
const activeCount = groups.filter(name => {
|
||||
return document.querySelectorAll(`input[name="${name}"]:checked`).length > 0;
|
||||
}).length;
|
||||
|
||||
const label = btn.querySelector('span');
|
||||
if (label) {
|
||||
label.textContent = activeCount > 0 ? `Filter (${activeCount})` : 'Filter';
|
||||
}
|
||||
btn.classList.toggle('has-filters', activeCount > 0);
|
||||
}
|
||||
|
||||
renderArchiveList() {
|
||||
async openArchiveModal() {
|
||||
this.handleModalOpen({ modalId: 'archive-modal' });
|
||||
|
||||
const archiveList = $('#archive-list');
|
||||
const archiveEmpty = $('#archive-empty');
|
||||
|
||||
// Ladeindikator anzeigen
|
||||
if (archiveList) {
|
||||
archiveList.classList.remove('hidden');
|
||||
archiveList.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--text-secondary)">Lade archivierte Aufgaben...</div>';
|
||||
}
|
||||
if (archiveEmpty) archiveEmpty.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const projectId = store.get('currentProjectId');
|
||||
const allTasks = await api.getTasks(projectId, { archived: true });
|
||||
const archivedTasks = allTasks.filter(t => t.archived);
|
||||
this.renderArchiveList(archivedTasks);
|
||||
} catch (error) {
|
||||
console.error('Failed to load archived tasks:', error);
|
||||
if (archiveList) {
|
||||
archiveList.innerHTML = '<div style="text-align:center;padding:2rem;color:var(--danger)">Fehler beim Laden der archivierten Aufgaben</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderArchiveList(tasks) {
|
||||
const archiveList = $('#archive-list');
|
||||
const archiveEmpty = $('#archive-empty');
|
||||
if (!archiveList) return;
|
||||
|
||||
// Get all archived tasks
|
||||
const tasks = store.get('tasks').filter(t => t.archived);
|
||||
const columns = store.get('columns');
|
||||
|
||||
if (tasks.length === 0) {
|
||||
@@ -855,13 +915,16 @@ class App {
|
||||
const projectId = store.get('currentProjectId');
|
||||
await api.restoreTask(projectId, taskId);
|
||||
store.updateTask(taskId, { archived: false });
|
||||
this.renderArchiveList();
|
||||
this.showSuccess('Aufgabe wiederhergestellt');
|
||||
|
||||
// Re-render board
|
||||
if (this.board) {
|
||||
this.board.render();
|
||||
}
|
||||
|
||||
// Archivliste neu laden
|
||||
const allTasks = await api.getTasks(projectId, { archived: true });
|
||||
this.renderArchiveList(allTasks.filter(t => t.archived));
|
||||
} catch (error) {
|
||||
console.error('Restore error:', error);
|
||||
this.showError('Fehler beim Wiederherstellen');
|
||||
@@ -889,8 +952,11 @@ class App {
|
||||
const projectId = store.get('currentProjectId');
|
||||
await api.deleteTask(projectId, taskId);
|
||||
store.removeTask(taskId);
|
||||
this.renderArchiveList();
|
||||
this.showSuccess('Aufgabe gelöscht');
|
||||
|
||||
// Archivliste neu laden
|
||||
const allTasks = await api.getTasks(projectId, { archived: true });
|
||||
this.renderArchiveList(allTasks.filter(t => t.archived));
|
||||
} catch (error) {
|
||||
this.showError('Fehler beim Löschen');
|
||||
}
|
||||
@@ -944,9 +1010,9 @@ class App {
|
||||
import('./contacts.js').then(module => {
|
||||
if (module.contactsManager) {
|
||||
module.contactsManager.searchQuery = '';
|
||||
module.contactsManager.filterContacts();
|
||||
module.contactsManager.loadContacts();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
|
||||
// Cancel any pending server search
|
||||
if (searchAbortController) {
|
||||
@@ -1027,9 +1093,9 @@ class App {
|
||||
import('./contacts.js').then(module => {
|
||||
if (module.contactsManager) {
|
||||
module.contactsManager.searchQuery = value;
|
||||
module.contactsManager.filterContacts();
|
||||
module.contactsManager.loadContacts();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
// Immediate client-side filtering for tasks
|
||||
store.setFilter('search', value);
|
||||
@@ -1283,6 +1349,55 @@ class App {
|
||||
cancelBtn?.addEventListener('click', handleCancel);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// COLUMN SELECT DIALOG
|
||||
// =====================
|
||||
|
||||
showColumnSelectDialog({ message, columns, onSelect }) {
|
||||
const modal = $('#column-select-modal');
|
||||
const messageEl = $('#column-select-message');
|
||||
const dropdown = $('#column-select-dropdown');
|
||||
const confirmBtn = $('#column-select-ok');
|
||||
const cancelBtn = $('#column-select-cancel');
|
||||
|
||||
if (messageEl) messageEl.textContent = message;
|
||||
|
||||
// Spalten in Dropdown einfügen
|
||||
if (dropdown) {
|
||||
dropdown.innerHTML = '';
|
||||
columns.forEach(column => {
|
||||
const option = document.createElement('option');
|
||||
option.value = column.id;
|
||||
option.textContent = column.name;
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Show modal
|
||||
this.handleModalOpen({ modalId: 'column-select-modal' });
|
||||
|
||||
// One-time event handlers
|
||||
const handleConfirm = () => {
|
||||
const selectedColumnId = parseInt(dropdown?.value);
|
||||
this.handleModalClose({ modalId: 'column-select-modal' });
|
||||
if (onSelect && selectedColumnId) onSelect(selectedColumnId);
|
||||
cleanup();
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
this.handleModalClose({ modalId: 'column-select-modal' });
|
||||
cleanup();
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
confirmBtn?.removeEventListener('click', handleConfirm);
|
||||
cancelBtn?.removeEventListener('click', handleCancel);
|
||||
};
|
||||
|
||||
confirmBtn?.addEventListener('click', handleConfirm);
|
||||
cancelBtn?.addEventListener('click', handleCancel);
|
||||
}
|
||||
|
||||
// =====================
|
||||
// LIGHTBOX
|
||||
// =====================
|
||||
|
||||
@@ -26,9 +26,6 @@ class BoardManager {
|
||||
this.weekStripDate = this.getMonday(new Date());
|
||||
this.tooltip = null;
|
||||
|
||||
// Layout preferences
|
||||
this.multiColumnLayout = this.loadLayoutPreference();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -56,20 +53,6 @@ class BoardManager {
|
||||
this.renderWeekStrip();
|
||||
});
|
||||
|
||||
// Apply initial layout preference
|
||||
setTimeout(() => {
|
||||
this.applyLayoutClass();
|
||||
if (this.multiColumnLayout) {
|
||||
this.checkAndApplyDynamicLayout();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Re-check layout on window resize
|
||||
window.addEventListener('resize', debounce(() => {
|
||||
if (this.multiColumnLayout) {
|
||||
this.checkAndApplyDynamicLayout();
|
||||
}
|
||||
}, 250));
|
||||
}
|
||||
|
||||
// =====================
|
||||
@@ -163,12 +146,6 @@ class BoardManager {
|
||||
// Keyboard navigation
|
||||
document.addEventListener('keydown', (e) => this.handleKeyboard(e));
|
||||
|
||||
// Layout toggle button - use delegated event handling
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('#btn-toggle-layout')) {
|
||||
this.toggleLayout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =====================
|
||||
@@ -180,9 +157,6 @@ class BoardManager {
|
||||
|
||||
const columns = store.get('columns');
|
||||
clearElement(this.boardElement);
|
||||
|
||||
// Apply layout class
|
||||
this.applyLayoutClass();
|
||||
|
||||
columns.forEach(column => {
|
||||
const columnElement = this.createColumnElement(column);
|
||||
@@ -198,9 +172,9 @@ class BoardManager {
|
||||
'Statuskarte hinzufügen'
|
||||
]);
|
||||
this.boardElement.appendChild(addColumnBtn);
|
||||
|
||||
// Check dynamic layout after render
|
||||
setTimeout(() => this.checkAndApplyDynamicLayout(), 100);
|
||||
|
||||
// Emit event for mobile column navigation
|
||||
document.dispatchEvent(new CustomEvent('columns:updated'));
|
||||
}
|
||||
|
||||
createColumnElement(column) {
|
||||
@@ -506,9 +480,6 @@ class BoardManager {
|
||||
countElement.textContent = filteredTasks.length.toString();
|
||||
}
|
||||
});
|
||||
|
||||
// Check if dynamic layout adjustment is needed
|
||||
setTimeout(() => this.checkAndApplyDynamicLayout(), 100);
|
||||
}
|
||||
|
||||
// =====================
|
||||
@@ -1496,102 +1467,6 @@ class BoardManager {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// LAYOUT PREFERENCES
|
||||
// =====================
|
||||
|
||||
loadLayoutPreference() {
|
||||
const stored = localStorage.getItem('taskmate:boardLayout');
|
||||
return stored === 'multiColumn';
|
||||
}
|
||||
|
||||
saveLayoutPreference(multiColumn) {
|
||||
localStorage.setItem('taskmate:boardLayout', multiColumn ? 'multiColumn' : 'single');
|
||||
}
|
||||
|
||||
toggleLayout() {
|
||||
this.multiColumnLayout = !this.multiColumnLayout;
|
||||
this.saveLayoutPreference(this.multiColumnLayout);
|
||||
this.applyLayoutClass();
|
||||
this.checkAndApplyDynamicLayout();
|
||||
|
||||
this.showSuccess(this.multiColumnLayout
|
||||
? 'Mehrspalten-Layout aktiviert'
|
||||
: 'Einspalten-Layout aktiviert'
|
||||
);
|
||||
}
|
||||
|
||||
applyLayoutClass() {
|
||||
const toggleBtn = $('#btn-toggle-layout');
|
||||
|
||||
if (this.multiColumnLayout) {
|
||||
this.boardElement?.classList.add('multi-column-layout');
|
||||
toggleBtn?.classList.add('active');
|
||||
} else {
|
||||
this.boardElement?.classList.remove('multi-column-layout');
|
||||
toggleBtn?.classList.remove('active');
|
||||
|
||||
// Remove all dynamic classes when disabled
|
||||
const columns = this.boardElement?.querySelectorAll('.column');
|
||||
columns?.forEach(column => {
|
||||
const columnBody = column.querySelector('.column-body');
|
||||
columnBody?.classList.remove('dynamic-2-columns', 'dynamic-3-columns');
|
||||
column.classList.remove('expanded-2x', 'expanded-3x');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
checkAndApplyDynamicLayout() {
|
||||
if (!this.multiColumnLayout || !this.boardElement) return;
|
||||
|
||||
// Debug logging
|
||||
console.log('[Layout Check] Checking dynamic layout...');
|
||||
|
||||
// Check each column to see if scrolling is needed
|
||||
const columns = this.boardElement.querySelectorAll('.column');
|
||||
|
||||
columns.forEach(column => {
|
||||
const columnBody = column.querySelector('.column-body');
|
||||
if (!columnBody) return;
|
||||
|
||||
// Remove dynamic classes first
|
||||
columnBody.classList.remove('dynamic-2-columns', 'dynamic-3-columns');
|
||||
column.classList.remove('expanded-2x', 'expanded-3x');
|
||||
|
||||
// Force reflow to get accurate measurements
|
||||
columnBody.offsetHeight;
|
||||
|
||||
const scrollHeight = columnBody.scrollHeight;
|
||||
const clientHeight = columnBody.clientHeight;
|
||||
const hasOverflow = scrollHeight > clientHeight;
|
||||
|
||||
console.log('[Layout Check]', {
|
||||
column: column.dataset.columnId,
|
||||
scrollHeight,
|
||||
clientHeight,
|
||||
hasOverflow,
|
||||
ratio: scrollHeight / clientHeight
|
||||
});
|
||||
|
||||
// Check if content overflows
|
||||
if (hasOverflow && clientHeight > 0) {
|
||||
// Calculate how many columns we need based on content height
|
||||
const ratio = scrollHeight / clientHeight;
|
||||
|
||||
if (ratio > 2.5 && window.innerWidth >= 1800) {
|
||||
// Need 3 columns
|
||||
console.log('[Layout] Applying 3 columns');
|
||||
columnBody.classList.add('dynamic-3-columns');
|
||||
column.classList.add('expanded-3x');
|
||||
} else if (ratio > 1.1 && window.innerWidth >= 1400) {
|
||||
// Need 2 columns (reduced threshold to 1.1)
|
||||
console.log('[Layout] Applying 2 columns');
|
||||
columnBody.classList.add('dynamic-2-columns');
|
||||
column.classList.add('expanded-2x');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export singleton
|
||||
|
||||
@@ -712,7 +712,11 @@ class CalendarViewManager {
|
||||
|
||||
filteredTasks = filteredTasks.filter(task => {
|
||||
const filterCategory = columnFilterMap[task.columnId];
|
||||
// If no filter state exists for this category, default to showing "in_progress"
|
||||
// If column has 'in_progress' filter category, check if 'in_progress' filter is enabled
|
||||
if (filterCategory === 'in_progress') {
|
||||
return this.enabledFilters['in_progress'] !== false;
|
||||
}
|
||||
// For other categories, check their specific filter state
|
||||
if (this.enabledFilters[filterCategory] === undefined) {
|
||||
// Default: show in_progress, hide open and completed
|
||||
return filterCategory === 'in_progress';
|
||||
|
||||
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { $, $$ } from './utils.js';
|
||||
import { enhanceMobileSwipe } from './mobile-swipe.js';
|
||||
|
||||
class MobileManager {
|
||||
constructor() {
|
||||
@@ -21,6 +22,8 @@ class MobileManager {
|
||||
this.touchStartTime = 0;
|
||||
this.isSwiping = false;
|
||||
this.swipeDirection = null;
|
||||
this.currentColumnIndex = 0;
|
||||
this.swipeTarget = null; // 'board' or 'header'
|
||||
|
||||
// Touch drag & drop state
|
||||
this.touchDraggedElement = null;
|
||||
@@ -87,6 +90,9 @@ class MobileManager {
|
||||
this.updateUserInfo();
|
||||
});
|
||||
|
||||
// Enhance with new swipe functionality
|
||||
enhanceMobileSwipe(this);
|
||||
|
||||
console.log('[Mobile] Initialized');
|
||||
}
|
||||
|
||||
@@ -220,8 +226,18 @@ class MobileManager {
|
||||
$$('.view').forEach(v => {
|
||||
const viewName = v.id.replace('view-', '');
|
||||
const isActive = viewName === view;
|
||||
v.classList.toggle('active', isActive);
|
||||
v.classList.toggle('hidden', !isActive);
|
||||
|
||||
if (isActive) {
|
||||
v.classList.add('active');
|
||||
v.classList.remove('hidden');
|
||||
// Force display for mobile
|
||||
if (this.isMobile) {
|
||||
v.style.display = '';
|
||||
}
|
||||
} else {
|
||||
v.classList.remove('active');
|
||||
v.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Update mobile nav
|
||||
@@ -300,41 +316,42 @@ class MobileManager {
|
||||
bindSwipeEvents() {
|
||||
if (!this.mainContent) return;
|
||||
|
||||
this.mainContent.addEventListener('touchstart', (e) => this.handleSwipeStart(e), { passive: true });
|
||||
this.mainContent.addEventListener('touchmove', (e) => this.handleSwipeMove(e), { passive: false });
|
||||
this.mainContent.addEventListener('touchend', (e) => this.handleSwipeEnd(e), { passive: true });
|
||||
this.mainContent.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
|
||||
// Swipe für Board-Container (Spalten-Navigation)
|
||||
const boardContainer = document.querySelector('.board-container');
|
||||
if (boardContainer) {
|
||||
boardContainer.addEventListener('touchstart', (e) => this.handleBoardSwipeStart(e), { passive: true });
|
||||
boardContainer.addEventListener('touchmove', (e) => this.handleBoardSwipeMove(e), { passive: false });
|
||||
boardContainer.addEventListener('touchend', (e) => this.handleBoardSwipeEnd(e), { passive: true });
|
||||
boardContainer.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
|
||||
}
|
||||
|
||||
// Swipe für Header (View-Navigation)
|
||||
const header = document.querySelector('.header');
|
||||
if (header) {
|
||||
header.addEventListener('touchstart', (e) => this.handleHeaderSwipeStart(e), { passive: true });
|
||||
header.addEventListener('touchmove', (e) => this.handleHeaderSwipeMove(e), { passive: false });
|
||||
header.addEventListener('touchend', (e) => this.handleHeaderSwipeEnd(e), { passive: true });
|
||||
header.addEventListener('touchcancel', () => this.resetSwipe(), { passive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle swipe start
|
||||
* Handle board swipe start (für Spalten-Navigation)
|
||||
*/
|
||||
handleSwipeStart(e) {
|
||||
if (!this.isMobile) return;
|
||||
handleBoardSwipeStart(e) {
|
||||
if (!this.isMobile || this.currentView !== 'board') return;
|
||||
if (this.isMenuOpen || $('.modal-overlay:not(.hidden)')) return;
|
||||
|
||||
// Don't swipe if menu is open
|
||||
if (this.isMenuOpen) return;
|
||||
|
||||
// Don't swipe if modal is open
|
||||
if ($('.modal-overlay:not(.hidden)')) return;
|
||||
|
||||
// Don't swipe on specific interactive elements, but allow swipe in column-body
|
||||
// Don't swipe on interactive elements
|
||||
const target = e.target;
|
||||
if (target.closest('.modal') ||
|
||||
target.closest('.calendar-grid') ||
|
||||
target.closest('.knowledge-entry-list') ||
|
||||
target.closest('.list-table') ||
|
||||
target.closest('input') ||
|
||||
target.closest('textarea') ||
|
||||
if (target.closest('button') ||
|
||||
target.closest('input') ||
|
||||
target.closest('textarea') ||
|
||||
target.closest('select') ||
|
||||
target.closest('button') ||
|
||||
target.closest('a[href]') ||
|
||||
target.closest('.task-card .priority-stars') ||
|
||||
target.closest('.task-card .task-counts')) {
|
||||
target.closest('.task-card')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only single touch
|
||||
if (e.touches.length !== 1) return;
|
||||
|
||||
this.touchStartX = e.touches[0].clientX;
|
||||
@@ -342,12 +359,79 @@ class MobileManager {
|
||||
this.touchStartTime = Date.now();
|
||||
this.isSwiping = false;
|
||||
this.swipeDirection = null;
|
||||
this.swipeTarget = 'board';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle swipe move
|
||||
* Handle header swipe start (für View-Navigation)
|
||||
*/
|
||||
handleSwipeMove(e) {
|
||||
handleHeaderSwipeStart(e) {
|
||||
if (!this.isMobile) return;
|
||||
if (this.isMenuOpen || $('.modal-overlay:not(.hidden)')) return;
|
||||
|
||||
// Nur in der Header-Region swipen
|
||||
if (!e.target.closest('.header')) return;
|
||||
|
||||
// Nicht auf Buttons swipen
|
||||
if (e.target.closest('button') || e.target.closest('.view-tab')) return;
|
||||
|
||||
if (e.touches.length !== 1) return;
|
||||
|
||||
this.touchStartX = e.touches[0].clientX;
|
||||
this.touchStartY = e.touches[0].clientY;
|
||||
this.touchStartTime = Date.now();
|
||||
this.isSwiping = false;
|
||||
this.swipeDirection = null;
|
||||
this.swipeTarget = 'header';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle board swipe move
|
||||
*/
|
||||
handleBoardSwipeMove(e) {
|
||||
if (!this.isMobile || this.touchStartX === 0 || this.swipeTarget !== 'board') return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.touchCurrentX = touch.clientX;
|
||||
this.touchCurrentY = touch.clientY;
|
||||
|
||||
const deltaX = this.touchCurrentX - this.touchStartX;
|
||||
const deltaY = this.touchCurrentY - this.touchStartY;
|
||||
|
||||
// Determine direction
|
||||
if (!this.swipeDirection && (Math.abs(deltaX) > 10 || Math.abs(deltaY) > 10)) {
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) * 1.5) {
|
||||
this.swipeDirection = 'horizontal';
|
||||
this.isSwiping = true;
|
||||
document.body.classList.add('is-swiping');
|
||||
} else {
|
||||
this.swipeDirection = 'vertical';
|
||||
this.resetSwipe();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.swipeDirection !== 'horizontal') return;
|
||||
e.preventDefault();
|
||||
|
||||
// Visual feedback für Spalten-Navigation
|
||||
const columns = $$('.column');
|
||||
if (deltaX > this.SWIPE_THRESHOLD && this.currentColumnIndex > 0) {
|
||||
this.swipeIndicatorLeft?.classList.add('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
} else if (deltaX < -this.SWIPE_THRESHOLD && this.currentColumnIndex < columns.length - 1) {
|
||||
this.swipeIndicatorRight?.classList.add('visible');
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
} else {
|
||||
this.swipeIndicatorLeft?.classList.remove('visible');
|
||||
this.swipeIndicatorRight?.classList.remove('visible');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle header swipe move
|
||||
*/
|
||||
handleHeaderSwipeMove(e) {
|
||||
if (!this.isMobile || this.touchStartX === 0) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
@@ -412,10 +496,11 @@ class MobileManager {
|
||||
* Handle swipe end
|
||||
*/
|
||||
handleSwipeEnd() {
|
||||
if (!this.isSwiping || this.swipeDirection !== 'horizontal') {
|
||||
this.resetSwipe();
|
||||
return;
|
||||
}
|
||||
if (!this.isMobile || this.touchStartX === 0 || this.swipeTarget !== 'header') return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.touchCurrentX = touch.clientX;
|
||||
this.touchCurrentY = touch.clientY;
|
||||
|
||||
const deltaX = this.touchCurrentX - this.touchStartX;
|
||||
const deltaTime = Date.now() - this.touchStartTime;
|
||||
|
||||
@@ -32,10 +32,10 @@ class Store {
|
||||
// Filters
|
||||
filters: {
|
||||
search: '',
|
||||
priority: 'all',
|
||||
assignee: 'all',
|
||||
label: 'all',
|
||||
dueDate: 'all',
|
||||
priority: '',
|
||||
assignee: '',
|
||||
label: '',
|
||||
dueDate: '',
|
||||
archived: false
|
||||
},
|
||||
|
||||
@@ -155,10 +155,10 @@ class Store {
|
||||
labels: [],
|
||||
filters: {
|
||||
search: '',
|
||||
priority: 'all',
|
||||
assignee: 'all',
|
||||
label: 'all',
|
||||
dueDate: 'all',
|
||||
priority: '',
|
||||
assignee: '',
|
||||
label: '',
|
||||
dueDate: '',
|
||||
archived: false
|
||||
},
|
||||
searchResultIds: [],
|
||||
@@ -412,10 +412,10 @@ class Store {
|
||||
this.setState({
|
||||
filters: {
|
||||
search: '',
|
||||
priority: 'all',
|
||||
assignee: 'all',
|
||||
label: 'all',
|
||||
dueDate: 'all',
|
||||
priority: '',
|
||||
assignee: '',
|
||||
label: '',
|
||||
dueDate: '',
|
||||
archived: false
|
||||
}
|
||||
}, 'RESET_FILTERS');
|
||||
|
||||
@@ -49,6 +49,16 @@ class SyncManager {
|
||||
});
|
||||
|
||||
this.setupEventListeners();
|
||||
|
||||
// Update body offline class based on sync status
|
||||
store.subscribe('syncStatus', () => {
|
||||
const status = store.get('syncStatus');
|
||||
if (status === 'offline' || status === 'error') {
|
||||
document.body.classList.add('offline');
|
||||
} else {
|
||||
document.body.classList.remove('offline');
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[Sync] Failed to connect:', error);
|
||||
store.setSyncStatus('error');
|
||||
|
||||
@@ -587,10 +587,38 @@ class TaskModalManager {
|
||||
this.close();
|
||||
this.showSuccess('Aufgabe wiederhergestellt');
|
||||
} catch (error) {
|
||||
this.showError('Fehler beim Wiederherstellen');
|
||||
// Prüfen ob Spaltenauswahl erforderlich ist
|
||||
if (error.data?.requiresColumn) {
|
||||
this.showColumnSelectDialog();
|
||||
} else {
|
||||
this.showError('Fehler beim Wiederherstellen');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showColumnSelectDialog() {
|
||||
const columns = store.get('columns');
|
||||
|
||||
// Modal für Spaltenauswahl erstellen
|
||||
window.dispatchEvent(new CustomEvent('column-select:show', {
|
||||
detail: {
|
||||
message: 'Die ursprüngliche Spalte existiert nicht mehr. Bitte wählen Sie eine Spalte:',
|
||||
columns: columns,
|
||||
onSelect: async (columnId) => {
|
||||
try {
|
||||
const projectId = store.get('currentProjectId');
|
||||
await api.restoreTask(projectId, this.taskId, columnId);
|
||||
store.updateTask(this.taskId, { archived: false, columnId: columnId });
|
||||
this.close();
|
||||
this.showSuccess('Aufgabe wiederhergestellt');
|
||||
} catch (error) {
|
||||
this.showError('Fehler beim Wiederherstellen');
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async autoSaveDescription() {
|
||||
// Deprecated - use autoSaveTask instead
|
||||
await this.autoSaveTask();
|
||||
|
||||
@@ -492,37 +492,55 @@ export function filterTasks(tasks, filters, searchResultIds = [], columns = [])
|
||||
}
|
||||
}
|
||||
|
||||
// Priority filter
|
||||
if (filters.priority && filters.priority !== 'all') {
|
||||
if (task.priority !== filters.priority) return false;
|
||||
// Priority filter (string or array)
|
||||
if (filters.priority && filters.priority.length > 0) {
|
||||
const priorityFilter = Array.isArray(filters.priority) ? filters.priority : [filters.priority];
|
||||
if (!priorityFilter.includes(task.priority)) return false;
|
||||
}
|
||||
|
||||
// Assignee filter
|
||||
if (filters.assignee && filters.assignee !== 'all') {
|
||||
if (task.assignedTo !== parseInt(filters.assignee)) return false;
|
||||
// Assignee filter (string or array)
|
||||
if (filters.assignee && filters.assignee.length > 0) {
|
||||
const assigneeFilter = Array.isArray(filters.assignee) ? filters.assignee : [filters.assignee];
|
||||
const assigneeIds = assigneeFilter.map(id => parseInt(id));
|
||||
const taskAssigneeId = task.assignedTo;
|
||||
const taskAssignees = task.assignees?.map(a => a.id || a) || [];
|
||||
const matches = assigneeIds.some(id => id === taskAssigneeId || taskAssignees.includes(id));
|
||||
if (!matches) return false;
|
||||
}
|
||||
|
||||
// Label filter
|
||||
if (filters.label && filters.label !== 'all') {
|
||||
const hasLabel = task.labels?.some(l => l.id === parseInt(filters.label));
|
||||
// Label filter (string or array)
|
||||
if (filters.label && filters.label.length > 0) {
|
||||
const labelFilter = Array.isArray(filters.label) ? filters.label : [filters.label];
|
||||
const labelIds = labelFilter.map(id => parseInt(id));
|
||||
const hasLabel = task.labels?.some(l => labelIds.includes(l.id));
|
||||
if (!hasLabel) return false;
|
||||
}
|
||||
|
||||
// Due date filter
|
||||
if (filters.dueDate && filters.dueDate !== 'all' && filters.dueDate !== '') {
|
||||
const status = getDueDateStatus(task.dueDate);
|
||||
// Due date filter (string or array)
|
||||
if (filters.dueDate && filters.dueDate.length > 0) {
|
||||
const dueDateFilter = Array.isArray(filters.dueDate) ? filters.dueDate : [filters.dueDate];
|
||||
let matches = false;
|
||||
|
||||
// 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;
|
||||
for (const filterVal of dueDateFilter) {
|
||||
if (filterVal === 'none') {
|
||||
if (!task.dueDate) { matches = true; break; }
|
||||
} else if (filterVal === 'overdue') {
|
||||
const status = getDueDateStatus(task.dueDate);
|
||||
if (status === 'overdue' && !isTaskCompleted(task)) { matches = true; break; }
|
||||
} else if (filterVal === 'today') {
|
||||
const status = getDueDateStatus(task.dueDate);
|
||||
if (status === 'today') { matches = true; break; }
|
||||
} else if (filterVal === 'week') {
|
||||
if (task.dueDate) {
|
||||
const due = new Date(task.dueDate);
|
||||
const weekFromNow = new Date();
|
||||
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
||||
if (due <= weekFromNow) { matches = true; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matches) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren