/**
* TaskFlow v1.72 - App
* Copyright (c) 2026 Florian Hesse
* Fischer Str. 11, 16515 Oranienburg
* https://comnic-it.de
* Alle Rechte vorbehalten.
*/
// Data Structure
let users = [];
let projects = [];
let currentUser = null;
let currentProjectId = null;
let currentTodoView = 'active';
let currentProjectView = 'list';
const projectColors = [
'#667eea','#3b82f6','#06b6d4','#0d9488','#10b981',
'#84cc16','#f59e0b','#f97316','#ef4444','#ec4899',
'#8b5cf6','#6366f1'
];
// i18n
let translations = {};
let currentLang = 'de';
// Animated counter
function animateValue(el, target, suffix = '') {
const start = parseInt(el.textContent) || 0;
if (start === target) { el.textContent = target + suffix; return; }
const duration = 500;
const startTime = performance.now();
function step(now) {
const progress = Math.min((now - startTime) / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3); // easeOutCubic
el.textContent = Math.round(start + (target - start) * ease) + suffix;
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
function t(key) {
return translations[key] || key;
}
async function loadLanguage(lang) {
try {
const response = await fetch(`lang/${lang}.json`);
translations = await response.json();
currentLang = lang;
localStorage.setItem('taskflow_lang', lang);
document.documentElement.lang = lang;
translatePage();
updateLangButtons();
// Re-render dynamic content
renderDashboard();
renderProjects();
if (currentProjectId) {
renderProjectStats();
renderProjectTodos();
}
} catch (error) {
console.error('Language load error:', error);
}
}
function translatePage() {
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.getAttribute('data-i18n');
if (translations[key]) el.textContent = translations[key];
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
const key = el.getAttribute('data-i18n-placeholder');
if (translations[key]) el.placeholder = translations[key];
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
const key = el.getAttribute('data-i18n-title');
if (translations[key]) el.title = translations[key];
});
}
function updateLangButtons() {
document.getElementById('langBtnDe').classList.toggle('active', currentLang === 'de');
document.getElementById('langBtnEn').classList.toggle('active', currentLang === 'en');
const settingsDe = document.getElementById('settingsLangDe');
const settingsEn = document.getElementById('settingsLangEn');
if (settingsDe) settingsDe.classList.toggle('active', currentLang === 'de');
if (settingsEn) settingsEn.classList.toggle('active', currentLang === 'en');
}
function changeLanguage(lang) {
loadLanguage(lang);
if (currentUser) {
apiCall('savePreferences', { preferences: { lang } });
}
}
// API Helper
async function apiCall(action, data = {}) {
try {
const response = await fetch(`api.php?action=${action}&lang=${currentLang}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
return result;
} catch (error) {
console.error('API Error:', error);
return {success: false, message: t('data.connection_error')};
}
}
// Version & Updates
let appVersion = '';
async function loadVersion() {
const result = await apiCall('getVersion');
if (result.success && result.data) {
appVersion = result.data.version || '';
}
document.querySelectorAll('.copyright').forEach(el => {
const link = el.querySelector('a');
const linkHtml = link ? ' \u00b7 ' + link.outerHTML : '';
el.innerHTML = 'TaskFlow v' + appVersion + ' \u00a9 2026 Florian Hesse' + linkHtml;
});
const versionEl = document.getElementById('currentVersion');
if (versionEl) versionEl.textContent = 'v' + appVersion;
}
async function checkForUpdate() {
const btn = document.getElementById('checkUpdateBtn');
const status = document.getElementById('updateStatus');
const installBtn = document.getElementById('installUpdateBtn');
btn.disabled = true;
btn.textContent = t('settings.update_checking');
status.style.display = 'none';
installBtn.style.display = 'none';
const result = await apiCall('checkUpdate');
btn.disabled = false;
btn.textContent = t('settings.update_check');
if (result.success) {
status.style.display = 'block';
if (result.data.update_available) {
status.innerHTML = '
' +
t('settings.update_available') + ': v' + result.data.remote + '' +
'
';
installBtn.style.display = 'inline-flex';
} else {
status.innerHTML = '' +
t('settings.update_up_to_date') +
'
';
}
} else {
status.style.display = 'block';
status.innerHTML = '' +
(result.message || t('settings.update_error')) +
'
';
}
}
async function installUpdate() {
const installBtn = document.getElementById('installUpdateBtn');
const status = document.getElementById('updateStatus');
if (!await showConfirm(t('settings.update_confirm'), { icon: '๐', title: t('settings.update_title'), danger: false })) return;
installBtn.disabled = true;
installBtn.textContent = t('settings.update_installing');
const result = await apiCall('doUpdate');
if (result.success) {
status.innerHTML = '' +
result.data.message + ' (v' + result.data.version + ')' +
'
';
installBtn.style.display = 'none';
loadVersion();
setTimeout(() => location.reload(), 2000);
} else {
status.innerHTML = '' +
(result.message || t('settings.update_error')) +
'
';
installBtn.disabled = false;
installBtn.textContent = t('settings.update_install');
}
}
// Copyright Protection
(function() {
const _cf = () => 'TaskFlow' + (appVersion ? ' v' + appVersion : '') + ' \u00a9 2026 Florian Hesse \u00b7 comnic-it.de';
function _pc() {
document.querySelectorAll('.content-footer .copyright').forEach(el => {
if (!el.innerHTML.includes('Florian Hesse')) el.innerHTML = _cf();
});
document.querySelectorAll('.login-box .copyright').forEach(el => {
if (!el.innerHTML.includes('Florian Hesse')) el.innerHTML = _cf();
});
document.querySelectorAll('.content-footer').forEach(el => {
el.style.removeProperty('display');
el.style.removeProperty('visibility');
el.style.removeProperty('opacity');
el.style.removeProperty('height');
el.style.removeProperty('overflow');
});
if (document.getElementById('appContainer') && !document.querySelector('.content-footer')) {
const f = document.createElement('footer');
f.className = 'content-footer';
f.innerHTML = '' + _cf() + '
';
document.querySelector('.main-content').appendChild(f);
}
}
const _ob = new MutationObserver(_pc);
document.addEventListener('DOMContentLoaded', function() {
_pc();
_ob.observe(document.body, {childList: true, subtree: true, attributes: true, characterData: true});
});
setInterval(_pc, 3000);
})();
// Initialize
async function init() {
let savedLang = localStorage.getItem('taskflow_lang');
if (!savedLang) {
const browserLang = (navigator.language || navigator.userLanguage || 'de').substring(0, 2);
savedLang = browserLang === 'en' ? 'en' : 'de';
}
await loadLanguage(savedLang);
loadTheme();
loadDarkMode();
applyLogo();
loadVersion();
// Check for password reset token in URL
const urlParams = new URLSearchParams(window.location.search);
const resetToken = urlParams.get('resetToken');
if (resetToken) {
showPasswordResetForm(resetToken);
// Clean URL without reload
window.history.replaceState({}, document.title, window.location.pathname);
return;
}
// Check if user is logged in
const sessionResult = await apiCall('getSession');
if (sessionResult.success) {
currentUser = sessionResult.data;
await loadProjectsFromServer();
showApp();
}
}
// User Management
function showLogin() {
document.getElementById('loginScreen').style.display = 'flex';
document.getElementById('loginUsername').focus();
}
// Enter-Key auf Login-Feldern
document.addEventListener('DOMContentLoaded', () => {
['loginUsername', 'loginPassword'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('keydown', e => { if (e.key === 'Enter') login(); });
});
const resetId = document.getElementById('resetIdentifier');
if (resetId) resetId.addEventListener('keydown', e => { if (e.key === 'Enter') requestPasswordReset(); });
['resetNewPassword', 'resetConfirmPassword'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('keydown', e => { if (e.key === 'Enter') submitPasswordReset(); });
});
});
async function login() {
const username = document.getElementById('loginUsername').value.trim();
const password = document.getElementById('loginPassword').value;
if (!username || !password) {
showToast('Login', t('login.alert_fields'), 'warning');
return;
}
const result = await apiCall('login', {username, password});
if (result.success) {
currentUser = result.data;
await loadProjectsFromServer();
showApp();
} else {
showToast('Login', result.message || t('login.alert_failed'), 'error');
}
}
async function logout() {
if (await showConfirm(t('nav.logout_confirm'), { icon: '๐ช', danger: false })) {
await apiCall('logout');
currentUser = null;
projects = [];
document.getElementById('appContainer').classList.remove('active');
document.getElementById('loginUsername').value = '';
document.getElementById('loginPassword').value = '';
showLogin();
}
}
// Password Reset Flow
function showForgotPassword() {
document.getElementById('loginBox').style.display = 'none';
document.getElementById('forgotPasswordBox').style.display = 'block';
document.getElementById('resetPasswordBox').style.display = 'none';
document.getElementById('resetIdentifier').value = '';
document.getElementById('resetIdentifier').focus();
}
function backToLogin() {
document.getElementById('loginBox').style.display = 'block';
document.getElementById('forgotPasswordBox').style.display = 'none';
document.getElementById('resetPasswordBox').style.display = 'none';
}
async function requestPasswordReset() {
const identifier = document.getElementById('resetIdentifier').value.trim();
if (!identifier) {
showToast(t('reset.title'), t('reset.password_required'), 'warning');
return;
}
await apiCall('requestPasswordReset', { identifier });
showToast(t('reset.title'), t('reset.email_sent'), 'success');
backToLogin();
}
let pendingResetToken = null;
function showPasswordResetForm(token) {
pendingResetToken = token;
document.getElementById('loginScreen').style.display = 'flex';
document.getElementById('loginBox').style.display = 'none';
document.getElementById('forgotPasswordBox').style.display = 'none';
document.getElementById('resetPasswordBox').style.display = 'block';
document.getElementById('resetNewPassword').value = '';
document.getElementById('resetConfirmPassword').value = '';
document.getElementById('resetNewPassword').focus();
}
async function submitPasswordReset() {
const pw = document.getElementById('resetNewPassword').value;
const pw2 = document.getElementById('resetConfirmPassword').value;
if (!pw) {
showToast(t('reset.title'), t('reset.password_required'), 'warning');
return;
}
if (pw !== pw2) {
showToast(t('reset.title'), t('reset.password_mismatch'), 'warning');
return;
}
const result = await apiCall('resetPassword', { token: pendingResetToken, password: pw });
if (result.success) {
showToast(t('reset.title'), t('reset.success'), 'success');
pendingResetToken = null;
backToLogin();
} else {
showToast(t('reset.title'), result.message || t('reset.error'), 'error');
}
}
async function showApp() {
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('appContainer').classList.add('active');
document.getElementById('userName').textContent = currentUser.name;
document.getElementById('userAvatar').textContent = currentUser.name.charAt(0).toUpperCase();
const isAdmin = currentUser.role === 'admin';
const navUsersItem = document.getElementById('navUsersItem');
if (navUsersItem) navUsersItem.style.display = isAdmin ? '' : 'none';
const userRoleEl = document.getElementById('userRole');
if (userRoleEl) userRoleEl.textContent = isAdmin ? t('users.role_admin') : t('users.role_user');
// Load per-user preferences from server
await loadUserPreferences();
renderDashboard();
loadNotifications();
}
async function loadUserPreferences() {
const result = await apiCall('getPreferences');
if (result.success && result.data) {
const prefs = result.data;
if (prefs.theme) {
localStorage.setItem('taskflow_theme', prefs.theme);
changeTheme(prefs.theme, false);
}
if (prefs.darkMode !== undefined) {
const isDark = !!prefs.darkMode;
localStorage.setItem('taskflow_dark', isDark ? 'true' : 'false');
document.body.setAttribute('data-dark', isDark);
updateDarkModeUI(isDark);
}
if (prefs.lang && prefs.lang !== currentLang) {
await loadLanguage(prefs.lang);
}
}
}
// Project Management
async function loadProjectsFromServer() {
const result = await apiCall('getProjects');
if (result.success) {
projects = result.data || [];
}
}
function openFeedback() {
const repoUrl = 'https://github.com/floppy007/taskflow/issues/new/choose';
window.open(repoUrl, '_blank');
}
function openNewProjectModal() {
document.getElementById('newProjectColor').value = '';
renderColorPicker('newProjectColorPicker', 'newProjectColor', '');
document.getElementById('newProjectModal').classList.add('active');
document.getElementById('newProjectName').focus();
}
function renderColorPicker(containerId, inputId, activeColor) {
const container = document.getElementById(containerId);
container.innerHTML = projectColors.map(c =>
``
).join('');
}
function selectProjectColor(inputId, containerId, color) {
document.getElementById(inputId).value = color;
document.querySelectorAll(`#${containerId} .color-dot`).forEach(d => d.classList.remove('active'));
event.target.classList.add('active');
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
}
async function createProject() {
const name = document.getElementById('newProjectName').value.trim();
const desc = document.getElementById('newProjectDesc').value.trim();
if (!name) {
showToast(t('topbar.new_project'), t('projects.name_required'), 'warning');
return;
}
const color = document.getElementById('newProjectColor').value;
const result = await apiCall('createProject', {name, desc, color});
if (result.success) {
await loadProjectsFromServer();
document.getElementById('newProjectName').value = '';
document.getElementById('newProjectDesc').value = '';
closeModal('newProjectModal');
renderDashboard();
renderProjects();
} else {
showToast(t('topbar.new_project'), result.message || t('projects.create_error'), 'error');
}
}
// Views
function showDashboard() {
setActiveNav(0);
hideAllViews();
showViewAnimated('dashboardView');
renderDashboard();
}
function showProjects() {
setActiveNav(1);
hideAllViews();
showViewAnimated('projectsView');
renderProjects();
loadDeletedProjects();
}
async function showUsers() {
if (currentUser.role !== 'admin') {
showDashboard();
return;
}
setActiveNav(2);
hideAllViews();
showViewAnimated('usersView');
const result = await apiCall('getUsers');
if (result.success) {
users = result.data;
renderUsers();
}
// Show LDAP import button if LDAP is enabled
const importBtn = document.getElementById('importLdapUsersBtn');
if (importBtn) {
const ldapResult = await apiCall('getLdapConfig');
importBtn.style.display = (ldapResult.success && ldapResult.data && ldapResult.data.enabled) ? '' : 'none';
}
}
function showSettings() {
setActiveNav(3);
hideAllViews();
showViewAnimated('settingsView');
// Hide password card for LDAP users
const pwCard = document.getElementById('passwordChangeCard');
if (pwCard) {
pwCard.style.display = (currentUser.source === 'ldap') ? 'none' : '';
}
// Load LDAP and SMTP config for admins
loadLdapConfig();
loadSmtpConfig();
}
function hideAllViews() {
['dashboardView','projectsView','usersView','settingsView','projectDetailView'].forEach(id => {
const el = document.getElementById(id);
el.style.display = 'none';
el.style.opacity = '0';
el.style.transform = 'translateY(12px)';
});
}
function showViewAnimated(id) {
const el = document.getElementById(id);
el.style.display = 'block';
el.style.opacity = '0';
el.style.transform = 'translateY(12px)';
requestAnimationFrame(() => {
el.style.transition = 'opacity .3s ease, transform .3s ease';
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
});
}
function setActiveNav(index) {
document.querySelectorAll('.nav-item').forEach((item, i) => {
item.classList.toggle('active', i === index);
});
}
// Render Functions
function renderDashboard() {
const totalProjects = projects.length;
const allTodos = projects.flatMap(p => p.todos || []);
const activeTodos = allTodos.filter(t => !t.archived);
const openTodos = activeTodos.filter(t => !t.done).length;
const doneTodos = activeTodos.filter(t => t.done).length;
const progress = activeTodos.length ? Math.round((doneTodos / activeTodos.length) * 100) : 0;
animateValue(document.getElementById('statProjects'), totalProjects);
animateValue(document.getElementById('statOpen'), openTodos);
animateValue(document.getElementById('statDone'), doneTodos);
animateValue(document.getElementById('statProgress'), progress, '%');
document.getElementById('projectCount').textContent = totalProjects;
renderActivityFeed();
const container = document.getElementById('dashboardProjects');
if (projects.length === 0) {
container.innerHTML = `
๐
${escapeHtml(t('projects.empty_title'))}
${escapeHtml(t('projects.empty_text'))}
`;
return;
}
container.innerHTML = projects.map(p => {
const todos = p.todos || [];
const activeTodos = todos.filter(t => !t.archived);
const total = activeTodos.length;
const done = activeTodos.filter(t => t.done).length;
const open = total - done;
const pct = total ? Math.round((done / total) * 100) : 0;
const canDelete = canDeleteProject(p);
const canManage = canDeleteProject(p);
return `
๐
${canManage || canDelete ? `
${canManage ? `` : ''}
${canDelete ? `` : ''}
` : ''}
${escapeHtml(t('projects.progress'))}
${pct}%
${open}
${escapeHtml(t('projects.open'))}
${done}
${escapeHtml(t('projects.done'))}
${total}
${escapeHtml(t('projects.total'))}
`;
}).join('');
}
function canDeleteProject(p) {
if (currentUser.role === 'admin') return true;
if (p.members) {
return p.members.some(m => m.userId === currentUser.id && m.role === 'owner');
}
return p.createdBy === currentUser.id;
}
function renderProjects() {
const container = document.getElementById('projectsList');
if (projects.length === 0) {
container.innerHTML = `
๐
${escapeHtml(t('projects.empty_title'))}
${escapeHtml(t('projects.empty_text'))}
`;
return;
}
container.innerHTML = projects.map(p => {
const todos = p.todos || [];
const activeTodos = todos.filter(t => !t.archived);
const total = activeTodos.length;
const done = activeTodos.filter(t => t.done).length;
const open = total - done;
const pct = total ? Math.round((done / total) * 100) : 0;
const canDelete = canDeleteProject(p);
const canManage = canDeleteProject(p);
return `
๐
${canManage || canDelete ? `
${canManage ? `` : ''}
${canDelete ? `` : ''}
` : ''}
${escapeHtml(p.name)}
${escapeHtml(p.desc || t('projects.no_desc'))}
${escapeHtml(t('projects.progress'))}
${pct}%
${open}
${escapeHtml(t('projects.open'))}
${done}
${escapeHtml(t('projects.done'))}
${total}
${escapeHtml(t('projects.total'))}
`;
}).join('');
}
function renderUsers() {
const container = document.getElementById('usersList');
container.innerHTML = users.map(u => {
const role = u.role || 'admin';
const isAdmin = role === 'admin';
const badgeClass = isAdmin ? 'badge-admin' : 'badge-user';
const badgeLabel = isAdmin ? t('users.role_admin') : t('users.role_user');
const badgeIcon = isAdmin ? '๐ก๏ธ' : '๐ค';
const newRole = isAdmin ? 'user' : 'admin';
const source = u.source || 'local';
const sourceBadge = source === 'ldap'
? 'AD/LDAP'
: '' + t('users.source_local') + '';
const emailDisplay = u.email
? 'โ ' + escapeHtml(u.email) + '
'
: '';
return `
${u.name.charAt(0).toUpperCase()}
${escapeHtml(u.name)}
@${escapeHtml(u.username)}
${emailDisplay}
${sourceBadge}
${badgeIcon} ${escapeHtml(badgeLabel)}
${escapeHtml(t('users.created'))} ${new Date(u.createdAt).toLocaleDateString(currentLang === 'de' ? 'de-DE' : 'en-US')}
${u.id !== currentUser.id ? `
` : ''}
${u.id !== currentUser.id ? `
` : ''}
`;
}).join('');
}
async function deleteProject(id) {
if (await showConfirm(t('projects.delete_confirm'), { icon: '๐๏ธ', title: t('project_detail.delete') })) {
const result = await apiCall('deleteProject', {id});
if (result.success) {
await loadProjectsFromServer();
renderDashboard();
renderProjects();
} else {
showToast(t('project_detail.delete'), result.message || t('projects.delete_error'), 'error');
}
}
}
// Project Detail View Functions
function openProjectDetail(projectId) {
currentProjectId = projectId;
const project = projects.find(p => p.id === projectId);
if (!project) return;
hideAllViews();
showViewAnimated('projectDetailView');
document.getElementById('projectDetailTitle').textContent = project.name;
document.getElementById('projectDetailDesc').textContent = project.desc || t('projects.no_desc');
currentTodoView = 'active';
currentProjectView = 'kanban';
switchTodoView('active');
renderProjectStats();
switchProjectView('kanban');
renderMembers(project);
}
function backToProjects() {
currentProjectId = null;
showProjects();
}
function editProject() {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
document.getElementById('editProjectName').value = project.name;
document.getElementById('editProjectDesc').value = project.desc || '';
document.getElementById('editProjectColor').value = project.color || '';
renderColorPicker('editProjectColorPicker', 'editProjectColor', project.color || '');
document.getElementById('editProjectModal').classList.add('active');
document.getElementById('editProjectName').focus();
}
async function saveEditProject() {
const name = document.getElementById('editProjectName').value.trim();
if (!name) { showToast(t('modal.edit_project'), t('projects.name_required'), 'warning'); return; }
const result = await apiCall('updateProject', {
id: currentProjectId,
name,
desc: document.getElementById('editProjectDesc').value.trim(),
color: document.getElementById('editProjectColor').value
});
if (result.success) {
closeModal('editProjectModal');
await loadProjectsFromServer();
const updated = projects.find(p => p.id === currentProjectId);
document.getElementById('projectDetailTitle').textContent = updated.name;
document.getElementById('projectDetailDesc').textContent = updated.desc || t('projects.no_desc');
renderDashboard();
renderProjects();
} else {
showToast(t('modal.edit_project'), result.message || t('projects.edit_error'), 'error');
}
}
async function deleteCurrentProject() {
if (!await showConfirm(t('projects.delete_confirm'), { icon: '๐๏ธ', title: t('project_detail.delete') })) return;
const result = await apiCall('deleteProject', {id: currentProjectId});
if (result.success) {
await loadProjectsFromServer();
backToProjects();
renderDashboard();
} else {
showToast(t('project_detail.delete'), result.message || t('projects.delete_error'), 'error');
}
}
function renderProjectStats() {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const activeTodos = (project.todos || []).filter(t => !t.archived);
const total = activeTodos.length;
const done = activeTodos.filter(t => t.done).length;
const open = total - done;
const progress = total ? Math.round((done / total) * 100) : 0;
animateValue(document.getElementById('projectStatTotal'), total);
animateValue(document.getElementById('projectStatOpen'), open);
animateValue(document.getElementById('projectStatDone'), done);
animateValue(document.getElementById('projectStatProgress'), progress, '%');
}
function toggleNewTodoForm() {
const form = document.getElementById('newTodoFormCard');
const btn = document.getElementById('newTodoToggleBtn');
const isVisible = form.style.display !== 'none';
if (isVisible) {
form.style.opacity = '0';
form.style.transform = 'translateY(-10px)';
setTimeout(() => { form.style.display = 'none'; btn.style.display = 'inline-flex'; }, 200);
} else {
form.style.display = 'block';
form.style.opacity = '0';
form.style.transform = 'translateY(-10px)';
requestAnimationFrame(() => {
form.style.transition = 'opacity .2s, transform .2s';
form.style.opacity = '1';
form.style.transform = 'translateY(0)';
});
btn.style.display = 'none';
document.getElementById('newTodoText').focus();
}
}
async function addTodoToProject() {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const text = document.getElementById('newTodoText').value.trim();
if (!text) {
showToast(t('todos.new_title'), t('todos.alert_required'), 'warning');
return;
}
const category = document.getElementById('newTodoCategory').value;
const priority = document.getElementById('newTodoPriority').value;
const note = document.getElementById('newTodoNote').value.trim();
const dueDate = document.getElementById('newTodoDueDate').value || null;
const result = await apiCall('addTodo', {
projectId: currentProjectId,
text,
category,
priority,
note,
dueDate
});
if (result.success) {
await loadProjectsFromServer();
document.getElementById('newTodoText').value = '';
document.getElementById('newTodoNote').value = '';
document.getElementById('newTodoDueDate').value = '';
toggleNewTodoForm();
renderProjectStats();
renderProjectTodos();
renderDashboard();
} else {
showToast(t('todos.new_title'), result.message || t('todos.alert_add_error'), 'error');
}
}
function switchTodoView(view) {
currentTodoView = view;
const activeBtn = document.getElementById('viewActiveBtn');
const archiveBtn = document.getElementById('viewArchiveBtn');
if (view === 'active') {
activeBtn.style.background = 'var(--card)';
activeBtn.style.boxShadow = 'var(--shadow-sm)';
activeBtn.classList.remove('btn-ghost');
archiveBtn.style.background = 'transparent';
archiveBtn.style.boxShadow = 'none';
archiveBtn.classList.add('btn-ghost');
} else {
archiveBtn.style.background = 'var(--card)';
archiveBtn.style.boxShadow = 'var(--shadow-sm)';
archiveBtn.classList.remove('btn-ghost');
activeBtn.style.background = 'transparent';
activeBtn.style.boxShadow = 'none';
activeBtn.classList.add('btn-ghost');
}
renderProjectTodos();
}
function renderProjectTodos() {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const container = document.getElementById('todoListContainer');
const filterCat = document.getElementById('filterCategory').value;
const filterStat = document.getElementById('filterStatus').value;
let todos = (project.todos || []).filter(td => {
if (currentTodoView === 'active' && td.archived) return false;
if (currentTodoView === 'archive' && !td.archived) return false;
if (filterCat !== 'all' && td.category !== filterCat) return false;
if (filterStat === 'open' && td.done) return false;
if (filterStat === 'done' && !td.done) return false;
return true;
});
if (todos.length === 0) {
container.innerHTML = `
${currentTodoView === 'archive' ? '๐ฆ' : 'โ'}
${escapeHtml(currentTodoView === 'archive' ? t('todos.empty_archive') : t('todos.empty_active'))}
`;
return;
}
const grouped = {};
todos.forEach(todo => {
const cat = todo.category || 'Other';
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(todo);
});
const categoryIcons = {
Development: '๐ป',
Design: '๐จ',
Content: '๐',
Testing: '๐งช',
Meeting: '๐ฅ',
Other: '๐'
};
const priorityLabels = {
low: t('todos.priority_low'),
medium: t('todos.priority_medium'),
high: t('todos.priority_high')
};
let html = '';
Object.keys(grouped).forEach(category => {
html += `
${categoryIcons[category] || '๐'}
${category}
`;
grouped[category].forEach(todo => {
html += `
โฎโฎ
${todo.done ? 'โ' : ''}
${categoryIcons[todo.category] || '๐'} ${todo.category}
${priorityLabels[todo.priority]}
${todo.dueDate ? (() => { const di = getDueDateInfo(todo.dueDate); return `๐
${di.label}`; })() : ''}
${(todo.attachments && todo.attachments.length > 0) ? `๐ ${todo.attachments.length}` : ''}
${todo.createdBy ? `${escapeHtml(t('todos.created_by'))} @${escapeHtml(todo.createdBy)}` : ''}
${todo.done && todo.closedBy ? `${escapeHtml(t('todos.closed_by'))} @${escapeHtml(todo.closedBy)}${todo.closedAt ? ' (' + new Date(todo.closedAt).toLocaleDateString(currentLang === 'de' ? 'de-DE' : 'en-US') + ')' : ''}` : ''}
${todo.note ? `
${escapeHtml(todo.note)}
` : ''}
${renderInlineAttachments(todo)}
`;
});
html += '
';
});
container.innerHTML = html;
}
async function toggleTodo(todoId) {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const todo = (project.todos || []).find(t => t.id === todoId);
if (!todo) return;
const result = await apiCall('updateTodo', {
projectId: currentProjectId,
todoId,
updates: {done: !todo.done}
});
if (result.success) {
await loadProjectsFromServer();
renderProjectStats();
renderProjectTodos();
renderDashboard();
}
}
function editTodo(todoId) {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const todo = (project.todos || []).find(t => t.id === todoId);
if (!todo) return;
document.getElementById('editTodoId').value = todoId;
document.getElementById('editTodoText').value = todo.text;
document.getElementById('editTodoCategory').value = todo.category || 'Other';
document.getElementById('editTodoPriority').value = todo.priority || 'medium';
document.getElementById('editTodoDueDate').value = todo.dueDate || '';
document.getElementById('editTodoNote').value = todo.note || '';
// Render attachments in the modal
renderAttachments(todo.attachments || []);
initAttachmentDropZone();
document.getElementById('editTodoModal').classList.add('active');
document.getElementById('editTodoText').focus();
}
async function saveEditTodo() {
const todoId = parseInt(document.getElementById('editTodoId').value);
const text = document.getElementById('editTodoText').value.trim();
if (!text) { showToast(t('todos.edit_title'), t('todos.alert_required'), 'warning'); return; }
const result = await apiCall('updateTodo', {
projectId: currentProjectId,
todoId,
updates: {
text,
category: document.getElementById('editTodoCategory').value,
priority: document.getElementById('editTodoPriority').value,
dueDate: document.getElementById('editTodoDueDate').value || null,
note: document.getElementById('editTodoNote').value.trim()
}
});
if (result.success) {
closeModal('editTodoModal');
await loadProjectsFromServer();
renderProjectTodos();
if (currentProjectView === 'kanban') renderKanbanBoard();
renderDashboard();
}
}
async function archiveTodo(todoId) {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const todo = (project.todos || []).find(t => t.id === todoId);
if (!todo) return;
const result = await apiCall('updateTodo', {
projectId: currentProjectId,
todoId,
updates: {archived: !todo.archived}
});
if (result.success) {
await loadProjectsFromServer();
renderProjectStats();
renderProjectTodos();
renderDashboard();
}
}
async function deleteTodo(todoId) {
if (!await showConfirm(t('todos.delete_confirm'), { icon: '๐๏ธ', title: t('todos.delete_btn') })) return;
const result = await apiCall('deleteTodo', {
projectId: currentProjectId,
todoId
});
if (result.success) {
await loadProjectsFromServer();
renderProjectStats();
renderProjectTodos();
renderDashboard();
}
}
// Data Management
async function exportData() {
const result = await apiCall('exportData');
if (result.success) {
const data = JSON.stringify(result.data, null, 2);
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `taskflow_backup_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
showToast(t('settings.backup_title'), t('data.export_success'), 'success');
} else {
showToast(t('settings.backup_title'), result.message || t('data.export_failed'), 'error');
}
}
async function importData(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
try {
const data = JSON.parse(e.target.result);
if (!data.users || !data.projects) {
showToast(t('settings.backup_title'), t('data.import_invalid'), 'error');
return;
}
if (await showConfirm(t('data.import_confirm'), { icon: 'โ ๏ธ', title: t('settings.backup_title') })) {
const result = await apiCall('importData', data);
if (result.success) {
await loadProjectsFromServer();
renderDashboard();
showToast(t('settings.backup_title'), t('data.import_success'), 'success');
} else {
showToast(t('settings.backup_title'), result.message || t('data.import_failed'), 'error');
}
}
} catch (error) {
showToast(t('settings.backup_title'), t('data.import_read_error') + error.message, 'error');
}
};
reader.readAsText(file);
}
// Password Change
async function changePassword() {
const currentPw = document.getElementById('currentPassword').value;
const newPw = document.getElementById('newPassword').value;
const confirmPw = document.getElementById('confirmPassword').value;
if (!currentPw || !newPw || !confirmPw) {
showToast(t('settings.password_title'), t('settings.password_fields_required'), 'warning');
return;
}
if (newPw !== confirmPw) {
showToast(t('settings.password_title'), t('settings.password_mismatch'), 'warning');
return;
}
const result = await apiCall('changePassword', {currentPassword: currentPw, newPassword: newPw});
if (result.success) {
showToast(t('settings.password_title'), t('settings.password_success'), 'success');
document.getElementById('currentPassword').value = '';
document.getElementById('newPassword').value = '';
document.getElementById('confirmPassword').value = '';
} else {
showToast(t('settings.password_title'), result.message || t('settings.password_error'), 'error');
}
}
// User Creation
function openCreateUserForm() {
document.getElementById('createUserCard').style.display = 'block';
}
function closeCreateUserForm() {
document.getElementById('createUserCard').style.display = 'none';
document.getElementById('newUserName').value = '';
document.getElementById('newUserUsername').value = '';
document.getElementById('newUserPassword').value = '';
document.getElementById('newUserRole').value = 'user';
}
async function createUser() {
const name = document.getElementById('newUserName').value;
const username = document.getElementById('newUserUsername').value;
const password = document.getElementById('newUserPassword').value;
const role = document.getElementById('newUserRole').value;
const email = document.getElementById('newUserEmail') ? document.getElementById('newUserEmail').value.trim() : '';
if (!name || !username || !password) {
showToast(t('users.create_title'), t('register.alert_fields'), 'warning');
return;
}
const result = await apiCall('createUser', {name, username, password, role, email});
if (result.success) {
closeCreateUserForm();
showToast(t('users.create_title'), result.message, 'success');
const usersResult = await apiCall('getUsers');
if (usersResult.success) {
users = usersResult.data;
renderUsers();
}
} else {
showToast(t('users.create_title'), result.message || t('users.create_error'), 'error');
}
}
// User Deletion
async function deleteUser(id) {
if (!await showConfirm(t('users.delete_confirm'), { icon: '๐๏ธ', title: t('users.delete_btn') })) return;
const result = await apiCall('deleteUser', {id});
if (result.success) {
showToast(t('users.title'), result.message, 'success');
const usersResult = await apiCall('getUsers');
if (usersResult.success) {
users = usersResult.data;
renderUsers();
}
} else {
showToast(t('users.title'), result.message || t('users.delete_error'), 'error');
}
}
// Toggle User Role
async function toggleUserRole(id, newRole) {
const result = await apiCall('updateUserRole', {id, role: newRole});
if (result.success) {
const usersResult = await apiCall('getUsers');
if (usersResult.success) {
users = usersResult.data;
renderUsers();
}
} else {
showToast(t('users.title'), result.message || t('users.no_permission'), 'error');
}
}
function applyLogo() {
const logoDataUrl = localStorage.getItem('taskflow_logo');
const loginLogo = document.getElementById('loginLogo');
const forgotLogo = document.getElementById('forgotLogo');
const resetLogo = document.getElementById('resetLogo');
const sidebarLogo = document.getElementById('sidebarLogo');
const logoSrc = logoDataUrl || 'logo.png';
if (loginLogo) loginLogo.innerHTML = `
`;
if (forgotLogo) forgotLogo.innerHTML = `
`;
if (resetLogo) resetLogo.innerHTML = `
`;
if (sidebarLogo) sidebarLogo.innerHTML = ``;
}
// Theme Management
function changeTheme(theme, save = true) {
document.body.setAttribute('data-theme', theme);
localStorage.setItem('taskflow_theme', theme);
document.querySelectorAll('.theme-option').forEach(opt => {
opt.classList.remove('active');
});
const opt = document.querySelector(`.theme-option[data-theme="${theme}"]`);
if (opt) opt.classList.add('active');
if (save && currentUser) {
apiCall('savePreferences', { preferences: { theme } });
}
}
function loadTheme() {
const savedTheme = localStorage.getItem('taskflow_theme') || 'purple';
changeTheme(savedTheme, false);
}
// Due Date Helper
function getDueDateInfo(dueDate) {
if (!dueDate) return null;
const now = new Date(); now.setHours(0,0,0,0);
const due = new Date(dueDate); due.setHours(0,0,0,0);
const diff = Math.ceil((due - now) / (1000*60*60*24));
if (diff < 0) return { class: 'overdue', label: t('todos.due_overdue') };
if (diff === 0) return { class: 'today', label: t('todos.due_today') };
if (diff <= 3) return { class: 'upcoming', label: due.toLocaleDateString(currentLang === 'de' ? 'de-DE' : 'en-US') };
return { class: 'later', label: due.toLocaleDateString(currentLang === 'de' ? 'de-DE' : 'en-US') };
}
// Dark Mode (auto-detect system, user can override via topbar)
function toggleDarkMode() {
const isDark = document.body.getAttribute('data-dark') === 'true';
const newVal = !isDark;
document.body.setAttribute('data-dark', newVal);
localStorage.setItem('taskflow_dark', newVal ? 'true' : 'false');
updateDarkModeUI(newVal);
if (currentUser) {
apiCall('savePreferences', { preferences: { darkMode: newVal } });
}
}
function loadDarkMode() {
const saved = localStorage.getItem('taskflow_dark');
let isDark;
if (saved !== null) {
isDark = saved === 'true';
} else {
isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
document.body.setAttribute('data-dark', isDark);
updateDarkModeUI(isDark);
// Listen for system theme changes (only if user hasn't overridden)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
if (localStorage.getItem('taskflow_dark') === null) {
document.body.setAttribute('data-dark', e.matches);
updateDarkModeUI(e.matches);
}
});
}
function updateDarkModeUI(isDark) {
const toggle = document.getElementById('darkModeToggle');
if (toggle) toggle.textContent = isDark ? 'โ๏ธ' : '๐';
}
// Kanban Board
function switchProjectView(view) {
currentProjectView = view;
const listBtn = document.getElementById('listViewBtn');
const kanbanBtn = document.getElementById('kanbanViewBtn');
const kanbanContainer = document.getElementById('kanbanContainer');
const newTodoBtn = document.getElementById('newTodoToggleBtn');
const newTodoForm = document.getElementById('newTodoFormCard');
const todoCard = document.getElementById('todoListContainer')?.closest('.card');
if (view === 'list') {
listBtn.style.background = 'var(--card)'; listBtn.style.boxShadow = 'var(--shadow-sm)'; listBtn.classList.remove('btn-ghost');
kanbanBtn.style.background = 'transparent'; kanbanBtn.style.boxShadow = 'none'; kanbanBtn.classList.add('btn-ghost');
if (todoCard) todoCard.style.display = '';
if (newTodoBtn) newTodoBtn.style.display = '';
kanbanContainer.style.display = 'none';
renderProjectTodos();
} else {
kanbanBtn.style.background = 'var(--card)'; kanbanBtn.style.boxShadow = 'var(--shadow-sm)'; kanbanBtn.classList.remove('btn-ghost');
listBtn.style.background = 'transparent'; listBtn.style.boxShadow = 'none'; listBtn.classList.add('btn-ghost');
if (newTodoForm) newTodoForm.style.display = 'none';
if (newTodoBtn) newTodoBtn.style.display = 'none';
if (todoCard) todoCard.style.display = 'none';
kanbanContainer.style.display = 'block';
renderKanbanBoard();
}
}
function renderKanbanBoard() {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const container = document.getElementById('kanbanContainer');
const todos = (project.todos || []).filter(td => !td.archived);
const columns = {
todo: { title: t('kanban.col_todo'), icon: '๐', items: [] },
inprogress: { title: t('kanban.col_inprogress'), icon: '๐', items: [] },
done: { title: t('kanban.col_done'), icon: 'โ
', items: [] }
};
todos.forEach(td => {
const status = td.status || (td.done ? 'done' : 'todo');
if (columns[status]) columns[status].items.push(td);
else columns.todo.items.push(td);
});
const categoryIcons = { Development:'๐ป', Design:'๐จ', Content:'๐', Testing:'๐งช', Meeting:'๐ฅ', Other:'๐' };
const priorityLabels = { low: t('todos.priority_low'), medium: t('todos.priority_medium'), high: t('todos.priority_high') };
container.innerHTML = '' +
Object.entries(columns).map(([status, col]) => `
${col.icon} ${col.title}
${col.items.length}
${col.items.map(td => `
${escapeHtml(td.text)}
${categoryIcons[td.category]||'๐'} ${td.category}
${priorityLabels[td.priority]}
${td.dueDate ? (() => { const di = getDueDateInfo(td.dueDate); return `๐
${di.label}`; })() : ''}
${(td.attachments && td.attachments.length > 0) ? `๐ ${td.attachments.length}` : ''}
${renderInlineAttachments(td)}
`).join('')}
`).join('') + '
';
}
async function dropKanbanCard(event, newStatus) {
event.preventDefault();
const todoId = parseInt(event.dataTransfer.getData('text/plain'));
const updates = { status: newStatus };
if (newStatus === 'done') updates.done = true;
else updates.done = false;
const result = await apiCall('updateTodo', {
projectId: currentProjectId,
todoId,
updates
});
if (result.success) {
await loadProjectsFromServer();
renderKanbanBoard();
renderProjectStats();
renderDashboard();
}
}
// Global Search
let searchTimeout = null;
function onSearchInput() {
clearTimeout(searchTimeout);
const query = document.getElementById('globalSearchInput').value.trim().toLowerCase();
if (query.length < 2) {
document.getElementById('searchResults').classList.remove('active');
return;
}
searchTimeout = setTimeout(() => performSearch(query), 250);
}
function performSearch(query) {
const results = [];
projects.forEach(p => {
if (p.name.toLowerCase().includes(query)) {
results.push({ type: 'project', projectId: p.id, title: p.name, context: p.desc || '' });
}
(p.todos || []).forEach(td => {
if (td.archived) return;
const matchText = td.text.toLowerCase().includes(query);
const matchNote = (td.note || '').toLowerCase().includes(query);
if (matchText || matchNote) {
results.push({
type: 'todo', projectId: p.id, todoId: td.id, title: td.text,
context: p.name + (matchNote ? ' - ' + td.note.substring(0, 80) : '')
});
}
});
});
const container = document.getElementById('searchResults');
if (results.length === 0) {
container.innerHTML = `${escapeHtml(t('search.no_results'))}
`;
container.classList.add('active');
return;
}
container.innerHTML = results.slice(0, 15).map(r => `
${r.type === 'project' ? '๐ ' + t('search.type_project') : 'โ ' + t('search.type_task')}
${escapeHtml(r.title)}
${escapeHtml(r.context)}
`).join('');
container.classList.add('active');
}
function navigateToSearchResult(projectId) {
document.getElementById('searchResults').classList.remove('active');
document.getElementById('globalSearchInput').value = '';
openProjectDetail(projectId);
}
document.addEventListener('click', (e) => {
if (!e.target.closest('.search-bar')) {
const sr = document.getElementById('searchResults');
if (sr) sr.classList.remove('active');
}
});
// Activity Feed
async function renderActivityFeed() {
const container = document.getElementById('activityFeed');
if (!container) return;
const result = await apiCall('getActivity', { count: 20 });
if (!result.success || !result.data || result.data.length === 0) {
container.innerHTML = `${escapeHtml(t('activity.empty'))}
`;
return;
}
const actionIcons = {
user_login: '๐', project_created: '๐', project_deleted: '๐๏ธ',
todo_created: 'โ', todo_completed: 'โ
', todo_deleted: 'โ'
};
const actionLabels = {
user_login: t('activity.user_login'), project_created: t('activity.project_created'),
project_deleted: t('activity.project_deleted'), todo_created: t('activity.todo_created'),
todo_completed: t('activity.todo_completed'), todo_deleted: t('activity.todo_deleted')
};
container.innerHTML = result.data.map(a => `
${actionIcons[a.action] || '๐'}
${escapeHtml(a.userName)} ${escapeHtml(actionLabels[a.action] || a.action)}
${a.projectName ? ' - ' + escapeHtml(a.projectName) + '' : ''}
${a.todoText ? ': ' + escapeHtml(a.todoText) : ''}
${timeAgo(a.timestamp)}
`).join('');
}
function timeAgo(dateStr) {
const now = new Date();
const date = new Date(dateStr);
const seconds = Math.floor((now - date) / 1000);
if (seconds < 60) return t('activity.just_now');
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return minutes + ' ' + t('activity.minutes_ago');
const hours = Math.floor(minutes / 60);
if (hours < 24) return hours + ' ' + t('activity.hours_ago');
const days = Math.floor(hours / 24);
if (days < 7) return days + ' ' + t('activity.days_ago');
return date.toLocaleDateString(currentLang === 'de' ? 'de-DE' : 'en-US');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
// Todo Drag & Drop Reorder
let draggedTodoId = null;
function todoDragStart(e) {
draggedTodoId = parseInt(e.currentTarget.dataset.todoId);
e.currentTarget.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function todoDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const item = e.currentTarget.closest('.todo-item');
if (item) {
document.querySelectorAll('.todo-item.drag-over').forEach(el => el.classList.remove('drag-over'));
item.classList.add('drag-over');
}
}
function todoDragEnd(e) {
e.currentTarget.classList.remove('dragging');
document.querySelectorAll('.todo-item.drag-over').forEach(el => el.classList.remove('drag-over'));
draggedTodoId = null;
}
async function todoDrop(e) {
e.preventDefault();
const targetItem = e.currentTarget.closest('.todo-item');
if (!targetItem) return;
const targetId = parseInt(targetItem.dataset.todoId);
if (draggedTodoId === null || draggedTodoId === targetId) return;
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const todos = project.todos || [];
const todoIds = todos.map(t => t.id);
const fromIdx = todoIds.indexOf(draggedTodoId);
const toIdx = todoIds.indexOf(targetId);
if (fromIdx === -1 || toIdx === -1) return;
todoIds.splice(fromIdx, 1);
todoIds.splice(toIdx, 0, draggedTodoId);
const result = await apiCall('reorderTodos', { projectId: currentProjectId, todoIds });
if (result.success) {
await loadProjectsFromServer();
renderProjectTodos();
}
}
// Toast Notifications
function showToast(title, msg, type = 'info') {
const container = document.getElementById('toastContainer');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.style.display = 'flex';
toast.style.alignItems = 'flex-start';
toast.style.gap = '12px';
toast.style.padding = '14px 16px';
const icons = { success: 'โ
', error: 'โ', warning: 'โ ๏ธ', info: 'โน๏ธ' };
toast.innerHTML = `
${icons[type] || icons.info}
${escapeHtml(title)}
${escapeHtml(msg)}
`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('toast-out');
setTimeout(() => toast.remove(), 300);
}, 5000);
}
// Custom Confirm Dialog (replaces browser confirm())
function showConfirm(message, { title = '', icon = 'โ ๏ธ', yesText = 'OK', noText = '', danger = true } = {}) {
return new Promise(resolve => {
const modal = document.getElementById('confirmModal');
document.getElementById('confirmIcon').textContent = icon;
document.getElementById('confirmTitle').textContent = title || t('modal.confirm_title') || 'Bestรคtigung';
document.getElementById('confirmMessage').textContent = message;
const yesBtn = document.getElementById('confirmYesBtn');
const noBtn = document.getElementById('confirmNoBtn');
yesBtn.textContent = yesText || 'OK';
noBtn.textContent = noText || t('modal.cancel') || 'Abbrechen';
yesBtn.className = danger ? 'btn btn-danger' : 'btn btn-primary';
function cleanup(result) {
modal.classList.remove('active');
yesBtn.removeEventListener('click', onYes);
noBtn.removeEventListener('click', onNo);
resolve(result);
}
function onYes() { cleanup(true); }
function onNo() { cleanup(false); }
yesBtn.addEventListener('click', onYes);
noBtn.addEventListener('click', onNo);
modal.classList.add('active');
});
}
// Member Management
async function openAddMemberModal() {
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const result = await apiCall('getAllUsers');
if (!result.success) return;
const allUsers = result.data || [];
const memberIds = (project.members || []).map(m => m.userId);
const available = allUsers.filter(u => !memberIds.includes(u.id));
const select = document.getElementById('addMemberUserId');
if (available.length === 0) {
select.innerHTML = ``;
} else {
select.innerHTML = available.map(u =>
``
).join('');
}
document.getElementById('addMemberRole').value = 'editor';
document.getElementById('addMemberModal').classList.add('active');
}
async function addMember() {
const userId = document.getElementById('addMemberUserId').value;
if (!userId) return;
const role = document.getElementById('addMemberRole').value;
const result = await apiCall('addMember', {
projectId: currentProjectId,
userId: parseInt(userId),
role
});
if (result.success) {
closeModal('addMemberModal');
await loadProjectsFromServer();
const project = projects.find(p => p.id === currentProjectId);
if (project) renderMembers(project);
showToast(t('members.title'), result.message, 'success');
} else {
showToast(t('members.add_error'), result.message || t('members.add_error'), 'error');
}
}
async function removeMember(projectId, userId) {
if (!await showConfirm(t('members.remove_confirm'), { icon: '๐๏ธ', title: t('members.remove_btn') })) return;
const result = await apiCall('removeMember', { projectId, userId });
if (result.success) {
await loadProjectsFromServer();
const project = projects.find(p => p.id === projectId);
if (project) renderMembers(project);
showToast(t('members.title'), result.message, 'success');
} else {
showToast(t('members.remove_error'), result.message || t('members.remove_error'), 'error');
}
}
async function updateMemberRole(projectId, userId, newRole) {
const result = await apiCall('updateMemberRole', { projectId, userId, role: newRole });
if (result.success) {
await loadProjectsFromServer();
const project = projects.find(p => p.id === projectId);
if (project) renderMembers(project);
} else {
showToast(t('members.role_error'), result.message || t('members.role_error'), 'error');
}
}
function renderMembers(project) {
const container = document.getElementById('membersList');
const addBtn = document.getElementById('addMemberBtn');
if (!container) return;
const members = project.members || [];
const isAdmin = currentUser.role === 'admin';
const isOwner = members.some(m => m.userId === currentUser.id && m.role === 'owner');
const canManage = isAdmin || isOwner;
if (addBtn) addBtn.style.display = canManage ? '' : 'none';
const roleLabels = {
owner: t('members.role_owner'),
editor: t('members.role_editor'),
viewer: t('members.role_viewer')
};
const roleIcons = { owner: '๐', editor: 'โ๏ธ', viewer: '๐๏ธ' };
container.innerHTML = members.map(m => {
const isMe = m.userId === currentUser.id;
const memberName = m.userName || m.username || `User #${m.userId}`;
const nextRole = m.role === 'editor' ? 'viewer' : 'editor';
const roleColor = m.role === 'owner' ? 'var(--warning)' : m.role === 'editor' ? 'var(--primary)' : 'var(--text-muted)';
return `
${escapeHtml(memberName.charAt(0).toUpperCase())}
${escapeHtml(memberName)}${isMe ? ' ' + escapeHtml(t('members.you')) + '' : ''}
${canManage && m.role !== 'owner' ? `
` : `
${roleIcons[m.role]} ${escapeHtml(roleLabels[m.role])}
`}
`;
}).join('');
}
// Manage Members Modal (standalone, works from project cards)
let manageMembersProjectId = null;
async function openManageMembersModal(projectId) {
manageMembersProjectId = projectId;
const project = projects.find(p => p.id === projectId);
if (!project) return;
renderManageMembersModal(project);
// Load available users for the add dropdown
const result = await apiCall('getAllUsers');
if (result.success) {
const allUsers = result.data || [];
const memberIds = (project.members || []).map(m => m.userId);
const available = allUsers.filter(u => !memberIds.includes(u.id));
const select = document.getElementById('manageMemberUserId');
const addSection = document.getElementById('manageMembersAdd');
if (available.length === 0) {
select.innerHTML = ``;
} else {
select.innerHTML = available.map(u =>
``
).join('');
}
}
document.getElementById('manageMembersModal').classList.add('active');
}
function renderManageMembersModal(project) {
const container = document.getElementById('manageMembersList');
const addSection = document.getElementById('manageMembersAdd');
if (!container) return;
const members = project.members || [];
const isAdmin = currentUser.role === 'admin';
const isOwner = members.some(m => m.userId === currentUser.id && m.role === 'owner');
const canManage = isAdmin || isOwner;
if (addSection) addSection.style.display = canManage ? '' : 'none';
const roleLabels = {
owner: t('members.role_owner'),
editor: t('members.role_editor'),
viewer: t('members.role_viewer')
};
const roleIcons = { owner: '๐', editor: 'โ๏ธ', viewer: '๐๏ธ' };
container.innerHTML = members.map(m => {
const isMe = m.userId === currentUser.id;
const memberName = m.userName || `User #${m.userId}`;
const nextRole = m.role === 'editor' ? 'viewer' : 'editor';
const roleColor = m.role === 'owner' ? 'var(--warning)' : m.role === 'editor' ? 'var(--primary)' : 'var(--text-muted)';
return `
${escapeHtml(memberName.charAt(0).toUpperCase())}
${escapeHtml(memberName)}${isMe ? ' ' + escapeHtml(t('members.you')) + '' : ''}
${canManage && m.role !== 'owner' ? `
` : `
${roleIcons[m.role]} ${escapeHtml(roleLabels[m.role])}
`}
`;
}).join('');
}
function selectMemberRole(role) {
document.getElementById('manageMemberRole').value = role;
document.querySelectorAll('#manageMemberRoleToggle .member-role-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.role === role);
});
}
async function addMemberFromModal() {
const userId = document.getElementById('manageMemberUserId').value;
if (!userId || !manageMembersProjectId) return;
const role = document.getElementById('manageMemberRole').value;
const result = await apiCall('addMember', {
projectId: manageMembersProjectId,
userId: parseInt(userId),
role
});
if (result.success) {
await loadProjectsFromServer();
const project = projects.find(p => p.id === manageMembersProjectId);
if (project) {
renderManageMembersModal(project);
renderMembers(project);
// Refresh the user dropdown
openManageMembersModal(manageMembersProjectId);
}
showToast(t('members.title'), result.message, 'success');
} else {
showToast(t('members.add_error'), result.message || t('members.add_error'), 'error');
}
}
async function removeMemberFromModal(projectId, userId) {
if (!await showConfirm(t('members.remove_confirm'), { icon: '๐๏ธ', title: t('members.remove_btn') })) return;
const result = await apiCall('removeMember', { projectId, userId });
if (result.success) {
await loadProjectsFromServer();
const project = projects.find(p => p.id === projectId);
if (project) {
renderManageMembersModal(project);
renderMembers(project);
// Refresh dropdown
openManageMembersModal(projectId);
}
showToast(t('members.title'), result.message, 'success');
} else {
showToast(t('members.remove_error'), result.message || t('members.remove_error'), 'error');
}
}
async function updateMemberFromModal(projectId, userId, newRole) {
const result = await apiCall('updateMemberRole', { projectId, userId, role: newRole });
if (result.success) {
await loadProjectsFromServer();
const project = projects.find(p => p.id === projectId);
if (project) {
renderManageMembersModal(project);
renderMembers(project);
}
} else {
showToast(t('members.role_error'), result.message || t('members.role_error'), 'error');
}
}
// LDAP Functions
async function loadLdapConfig() {
const card = document.getElementById('ldapSettingsCard');
if (!card || currentUser.role !== 'admin') return;
card.style.display = 'block';
const result = await apiCall('getLdapConfig');
if (result.success && result.data) {
const c = result.data;
document.getElementById('ldapServer').value = c.server || '';
document.getElementById('ldapPort').value = c.port || 389;
document.getElementById('ldapBaseDn').value = c.base_dn || '';
document.getElementById('ldapUserOu').value = c.user_ou || '';
document.getElementById('ldapBindUserDn').value = c.bind_user_dn || '';
document.getElementById('ldapBindPassword').value = c.bind_password || '';
document.getElementById('ldapSearchFilter').value = c.search_filter || '(&(objectClass=user)(objectCategory=person))';
document.getElementById('ldapUsernameAttr').value = c.username_attribute || 'sAMAccountName';
document.getElementById('ldapDisplaynameAttr').value = c.display_name_attribute || 'displayName';
document.getElementById('ldapEmailAttr').value = c.email_attribute || 'mail';
document.getElementById('ldapUseTls').checked = !!c.use_tls;
document.getElementById('ldapEnabled').checked = !!c.enabled;
}
}
async function saveLdapConfig() {
const config = {
server: document.getElementById('ldapServer').value.trim(),
port: parseInt(document.getElementById('ldapPort').value) || 389,
base_dn: document.getElementById('ldapBaseDn').value.trim(),
user_ou: document.getElementById('ldapUserOu').value.trim(),
bind_user_dn: document.getElementById('ldapBindUserDn').value.trim(),
bind_password: document.getElementById('ldapBindPassword').value,
search_filter: document.getElementById('ldapSearchFilter').value.trim(),
username_attribute: document.getElementById('ldapUsernameAttr').value.trim(),
display_name_attribute: document.getElementById('ldapDisplaynameAttr').value.trim(),
email_attribute: document.getElementById('ldapEmailAttr').value.trim(),
use_tls: document.getElementById('ldapUseTls').checked,
enabled: document.getElementById('ldapEnabled').checked
};
const result = await apiCall('saveLdapConfig', config);
if (result.success) {
showToast(t('ldap.title'), result.message, 'success');
} else {
showToast(t('ldap.title'), result.message || t('ldap.save_error'), 'error');
}
}
async function testLdapConnection() {
const resultDiv = document.getElementById('ldapTestResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = '' + t('ldap.testing') + '...
';
const result = await apiCall('testLdapConnection');
if (result.success) {
resultDiv.innerHTML = '' +
escapeHtml(result.message) + '
';
} else {
resultDiv.innerHTML = '' +
escapeHtml(result.message) + '
';
}
}
async function importLdapUsers() {
if (!await showConfirm(t('ldap.import_confirm'), { icon: '๐ฅ', title: t('ldap.import_btn'), danger: false })) return;
const result = await apiCall('importLdapUsers');
if (result.success) {
const d = result.data;
const detail = `${t('ldap.imported')}: ${d.imported}, ${t('ldap.updated')}: ${d.updated}, ${t('ldap.skipped')}: ${d.skipped}`;
showToast(t('ldap.import_btn'), detail, 'success');
// Refresh user list
const usersResult = await apiCall('getUsers');
if (usersResult.success) {
users = usersResult.data;
renderUsers();
}
} else {
showToast(t('ldap.import_btn'), result.message || t('ldap.import_error'), 'error');
}
}
// SMTP Config
async function loadSmtpConfig() {
const card = document.getElementById('smtpSettingsCard');
if (!card || currentUser.role !== 'admin') return;
card.style.display = 'block';
const result = await apiCall('getSmtpConfig');
if (result.success && result.data) {
const c = result.data;
document.getElementById('smtpHost').value = c.host || '';
document.getElementById('smtpPort').value = c.port || 587;
document.getElementById('smtpUsername').value = c.username || '';
document.getElementById('smtpPassword').value = c.password || '';
document.getElementById('smtpFromEmail').value = c.from_email || '';
document.getElementById('smtpFromName').value = c.from_name || 'TaskFlow';
document.getElementById('smtpEncryption').value = c.encryption || 'tls';
document.getElementById('smtpEnabled').checked = !!c.enabled;
}
}
async function saveSmtpConfig() {
const config = {
host: document.getElementById('smtpHost').value.trim(),
port: parseInt(document.getElementById('smtpPort').value) || 587,
username: document.getElementById('smtpUsername').value.trim(),
password: document.getElementById('smtpPassword').value,
from_email: document.getElementById('smtpFromEmail').value.trim(),
from_name: document.getElementById('smtpFromName').value.trim(),
encryption: document.getElementById('smtpEncryption').value,
enabled: document.getElementById('smtpEnabled').checked
};
const result = await apiCall('saveSmtpConfig', config);
if (result.success) {
showToast(t('smtp.title'), result.message, 'success');
} else {
showToast(t('smtp.title'), result.message || t('smtp.save_error'), 'error');
}
}
async function testSmtpConnection() {
const email = document.getElementById('smtpTestEmail').value.trim();
if (!email) {
showToast(t('smtp.title'), t('smtp.save_error'), 'warning');
return;
}
const resultDiv = document.getElementById('smtpTestResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = 'Sending...
';
const result = await apiCall('testSmtpConfig', { email });
if (result.success) {
resultDiv.innerHTML = '' +
escapeHtml(result.message) + '
';
} else {
resultDiv.innerHTML = '' +
escapeHtml(result.message) + '
';
}
}
// Notifications
async function loadNotifications() {
const result = await apiCall('getNotifications');
if (result.success && result.data && result.data.length > 0) {
showPendingNotifications(result.data);
}
}
async function showPendingNotifications(notifications) {
const ids = [];
notifications.forEach(n => {
ids.push(n.id);
if (n.type === 'project_added') {
const title = t('notifications.project_added_title');
const msg = t('notifications.project_added').replace('{name}', n.projectName || '');
showToast(title, msg, 'info');
}
});
if (ids.length > 0) {
await apiCall('dismissNotifications', { ids });
}
}
// Trash / Deleted Projects
async function loadDeletedProjects() {
const container = document.getElementById('deletedProjectsList');
const card = document.getElementById('trashCard');
if (!container) return;
const result = await apiCall('getDeletedProjects');
if (!result.success || !result.data || result.data.length === 0) {
if (card) card.style.display = 'none';
return;
}
if (card) card.style.display = 'block';
const isAdmin = currentUser.role === 'admin';
container.innerHTML = result.data.map(p => {
const deletedDate = new Date(p.deletedAt).toLocaleDateString(currentLang === 'de' ? 'de-DE' : 'en-US');
return `
๐
${escapeHtml(p.name)}
${escapeHtml(t('trash.deleted_by'))} ${escapeHtml(p.deletedByName)} · ${deletedDate} ·
${p.daysLeft} ${escapeHtml(t('trash.days_left'))}
${isAdmin ? `
` : ''}
`;
}).join('');
}
async function restoreProject(id) {
const result = await apiCall('restoreProject', { id });
if (result.success) {
showToast(t('trash.title'), t('trash.restored'), 'success');
await loadProjectsFromServer();
renderDashboard();
renderProjects();
loadDeletedProjects();
} else {
showToast(t('trash.title'), result.message, 'error');
}
}
async function permanentDeleteProject(id) {
if (!await showConfirm(t('trash.confirm_permanent'), { icon: 'โ ๏ธ', title: t('trash.delete_permanent') })) return;
const result = await apiCall('permanentDeleteProject', { id });
if (result.success) {
showToast(t('trash.title'), result.message, 'success');
loadDeletedProjects();
} else {
showToast(t('trash.title'), result.message, 'error');
}
}
// Attachments
function renderInlineAttachments(todo) {
const atts = todo.attachments;
if (!atts || atts.length === 0) return '';
const items = atts.map((att, idx) => {
const url = `api.php?action=downloadAttachment&projectId=${currentProjectId}&todoId=${todo.id}&attachmentId=${encodeURIComponent(att.id)}`;
const isImage = isImageFile(att.filename);
if (isImage) {
return `
`;
}
return `${getFileIcon(att.filename)} ${escapeHtml(att.filename.length > 18 ? att.filename.substring(0, 15) + '...' : att.filename)}`;
}).join('');
return ``;
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
function getFileIcon(filename) {
const ext = (filename || '').split('.').pop().toLowerCase();
const icons = {
pdf: '๐', doc: '๐', docx: '๐', xls: '๐', xlsx: '๐',
ppt: '๐', pptx: '๐', txt: '๐', csv: '๐',
png: '๐ผ๏ธ', jpg: '๐ผ๏ธ', jpeg: '๐ผ๏ธ', gif: '๐ผ๏ธ', svg: '๐ผ๏ธ', webp: '๐ผ๏ธ',
zip: '๐ฆ', rar: '๐ฆ', '7z': '๐ฆ', tar: '๐ฆ', gz: '๐ฆ',
mp4: '๐ฌ', avi: '๐ฌ', mov: '๐ฌ', mp3: '๐ต', wav: '๐ต',
};
return icons[ext] || '๐';
}
function isPreviewable(filename) {
return /\.(png|jpe?g|gif|svg|webp|bmp|pdf|txt|csv|mp4|webm|ogg|mp3|wav)$/i.test(filename);
}
function isImageFile(filename) {
return /\.(png|jpe?g|gif|svg|webp|bmp)$/i.test(filename);
}
function renderAttachments(attachments) {
const container = document.getElementById('attachmentList');
if (!container) return;
if (!attachments || attachments.length === 0) {
container.innerHTML = `${escapeHtml(t('attachments.none'))}
`;
return;
}
const todoId = parseInt(document.getElementById('editTodoId').value);
container.innerHTML = attachments.map((att, idx) => {
const url = `api.php?action=downloadAttachment&projectId=${currentProjectId}&todoId=${todoId}&attachmentId=${encodeURIComponent(att.id)}`;
const isImage = isImageFile(att.filename);
const canPreview = isPreviewable(att.filename);
return `
${isImage
? `

`
: `
${getFileIcon(att.filename)}
`
}
${escapeHtml(att.filename)}
${formatFileSize(att.size)} · ${att.uploadedBy ? '@' + escapeHtml(att.uploadedBy) : ''} · ${new Date(att.uploadedAt).toLocaleDateString(currentLang === 'de' ? 'de-DE' : 'en-US')}
${canPreview ? `
` : ''}
โฌ๏ธ
`;
}).join('');
}
function initAttachmentDropZone() {
const zone = document.getElementById('attachmentDropZone');
if (!zone || zone._attachmentInit) return;
zone._attachmentInit = true;
zone.addEventListener('dragover', (e) => {
e.preventDefault();
zone.classList.add('drag-over');
});
zone.addEventListener('dragleave', () => {
zone.classList.remove('drag-over');
});
zone.addEventListener('drop', (e) => {
e.preventDefault();
zone.classList.remove('drag-over');
if (e.dataTransfer.files.length > 0) {
handleAttachmentUpload(e.dataTransfer.files);
}
});
}
async function handleAttachmentUpload(files) {
if (!files || files.length === 0) return;
const file = files[0];
const maxSize = 10 * 1024 * 1024; // 10 MB
if (file.size > maxSize) {
showToast(t('attachments.title'), t('attachments.too_large'), 'warning');
document.getElementById('attachmentFileInput').value = '';
return;
}
const todoId = parseInt(document.getElementById('editTodoId').value);
const formData = new FormData();
formData.append('file', file);
formData.append('projectId', currentProjectId);
formData.append('todoId', todoId);
try {
const response = await fetch(`api.php?action=uploadAttachment&lang=${currentLang}`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
showToast(t('attachments.title'), t('attachments.uploaded'), 'success');
await loadProjectsFromServer();
// Re-render attachments
const project = projects.find(p => p.id === currentProjectId);
if (project) {
const todo = project.todos.find(t => t.id === todoId);
if (todo) renderAttachments(todo.attachments || []);
}
renderProjectTodos();
if (currentProjectView === 'kanban') renderKanbanBoard();
} else {
showToast(t('attachments.title'), result.message || t('attachments.upload_error'), 'error');
}
} catch (error) {
showToast(t('attachments.title'), t('attachments.upload_error'), 'error');
}
document.getElementById('attachmentFileInput').value = '';
}
async function deleteAttachment(attachmentId) {
if (!await showConfirm(t('attachments.delete_confirm'), { icon: '๐๏ธ', title: t('attachments.delete') })) return;
const todoId = parseInt(document.getElementById('editTodoId').value);
const result = await apiCall('deleteAttachment', {
projectId: currentProjectId,
todoId,
attachmentId
});
if (result.success) {
showToast(t('attachments.title'), t('attachments.deleted'), 'success');
await loadProjectsFromServer();
const project = projects.find(p => p.id === currentProjectId);
if (project) {
const todo = project.todos.find(t => t.id === todoId);
if (todo) renderAttachments(todo.attachments || []);
}
renderProjectTodos();
if (currentProjectView === 'kanban') renderKanbanBoard();
} else {
showToast(t('attachments.title'), result.message || t('attachments.delete_error'), 'error');
}
}
// Attachment Preview Lightbox
function previewAttachmentDirect(projectId, todoId, idx) {
const project = projects.find(p => p.id === projectId);
if (!project) return;
const todo = project.todos.find(t => t.id === todoId);
if (!todo || !todo.attachments || !todo.attachments[idx]) return;
showAttachmentPreview(todo.attachments[idx], projectId, todoId);
}
function previewAttachmentByIndex(idx) {
const todoId = parseInt(document.getElementById('editTodoId').value);
const project = projects.find(p => p.id === currentProjectId);
if (!project) return;
const todo = project.todos.find(t => t.id === todoId);
if (!todo || !todo.attachments || !todo.attachments[idx]) return;
showAttachmentPreview(todo.attachments[idx], currentProjectId, todoId);
}
function showAttachmentPreview(att, projectId, todoId) {
const url = `api.php?action=downloadAttachment&projectId=${projectId}&todoId=${todoId}&attachmentId=${encodeURIComponent(att.id)}`;
const filename = att.filename;
const ext = (filename || '').split('.').pop().toLowerCase();
let overlay = document.getElementById('attachmentPreviewOverlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'attachmentPreviewOverlay';
overlay.className = 'attachment-preview-overlay';
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closeAttachmentPreview();
});
}
overlay.querySelector('.attachment-preview-title').textContent = filename;
const dlBtn = overlay.querySelector('.attachment-preview-download');
dlBtn.href = url;
dlBtn.download = filename;
const body = overlay.querySelector('.attachment-preview-body');
// Render preview based on file type
if (isImageFile(filename)) {
body.innerHTML = `
`;
} else if (ext === 'pdf') {
body.innerHTML = ``;
} else if (['mp4','webm','ogg'].includes(ext)) {
body.innerHTML = ``;
} else if (['mp3','wav','ogg'].includes(ext)) {
body.innerHTML = `๐ต
${escapeHtml(filename)}
`;
} else if (['txt','csv','log','md'].includes(ext)) {
body.innerHTML = ``;
} else {
body.innerHTML = `${getFileIcon(filename)}
${escapeHtml(filename)}
${formatFileSize(att.size)}
โฌ๏ธ Download `;
}
overlay.classList.add('active');
}
function closeAttachmentPreview() {
const overlay = document.getElementById('attachmentPreviewOverlay');
if (overlay) {
overlay.classList.remove('active');
overlay.querySelector('.attachment-preview-body').innerHTML = '';
}
}
// Start app
init();