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:
@@ -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
|
||||
// =====================
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren